From 17472e7eb0f5f10c937dfb7973bd6f8365e16ebe Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 15:35:29 -0800 Subject: [PATCH 001/134] Copy proto-based changes from #2173. --- app/BUILD.bazel | 12 +- .../oppia/android/app/activity/BUILD.bazel | 2 +- config/config_proto_assets.bzl | 4 +- .../java/org/oppia/android/config/BUILD.bazel | 4 +- data/BUILD.bazel | 2 +- .../android/data/backends/gae/BUILD.bazel | 2 +- .../android/data/persistence/BUILD.bazel | 2 +- domain/BUILD.bazel | 12 +- domain/domain_assets.bzl | 24 +- .../oppia/android/domain/audio/BUILD.bazel | 2 +- .../oppia/android/domain/locale/BUILD.bazel | 6 +- .../android/domain/onboarding/BUILD.bazel | 2 +- .../android/domain/oppialogger/BUILD.bazel | 2 +- .../domain/oppialogger/analytics/BUILD.bazel | 2 +- .../domain/oppialogger/exceptions/BUILD.bazel | 2 +- .../oppia/android/domain/state/BUILD.bazel | 8 +- .../android/domain/translation/BUILD.bazel | 12 +- .../org/oppia/android/domain/util/BUILD.bazel | 4 +- .../oppia/android/domain/locale/BUILD.bazel | 6 +- .../android/domain/translation/BUILD.bazel | 2 +- model/BUILD.bazel | 269 +----------------- model/oppia_proto_library.bzl | 19 ++ model/src/main/proto/BUILD.bazel | 257 +++++++++++++++++ .../proto/format_import_proto_library.bzl | 44 --- model/text_proto_assets.bzl | 4 +- scripts/script_assets.bzl | 14 +- .../oppia/android/scripts/common/BUILD.bazel | 2 +- .../oppia/android/testing/junit/BUILD.bazel | 2 +- .../oppia/android/testing/data/BUILD.bazel | 2 +- utility/BUILD.bazel | 6 +- .../org/oppia/android/util/locale/BUILD.bazel | 4 +- .../oppia/android/util/logging/BUILD.bazel | 4 +- .../android/util/logging/firebase/BUILD.bazel | 4 +- .../android/util/caching/testing/BUILD.bazel | 2 +- .../org/oppia/android/util/locale/BUILD.bazel | 2 +- 35 files changed, 357 insertions(+), 390 deletions(-) create mode 100644 model/oppia_proto_library.bzl create mode 100644 model/src/main/proto/BUILD.bazel delete mode 100644 model/src/main/proto/format_import_proto_library.bzl diff --git a/app/BUILD.bazel b/app/BUILD.bazel index 0ae4da3c1ce..3e09ece2557 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -546,8 +546,8 @@ android_library( ":views", "//app/src/main/java/org/oppia/android/app/translation:app_language_activity_injector_provider", "//app/src/main/java/org/oppia/android/app/translation:app_language_resource_handler", - "//model:interaction_object_java_proto_lite", - "//model:thumbnail_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:thumbnail_java_proto_lite", "//third_party:androidx_annotation_annotation", "//third_party:androidx_constraintlayout_constraintlayout", "//third_party:androidx_core_core", @@ -579,8 +579,8 @@ kt_android_library( deps = [ ":dagger", "//domain/src/main/java/org/oppia/android/domain/audio:cellular_audio_dialog_controller", - "//model:question_java_proto_lite", - "//model:topic_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", + "//model/src/main/proto:topic_java_proto_lite", "//third_party:androidx_recyclerview_recyclerview", ], ) @@ -682,7 +682,7 @@ android_library( ":view_models", "//app/src/main/java/org/oppia/android/app/translation:app_language_activity_injector_provider", "//app/src/main/java/org/oppia/android/app/translation:app_language_resource_handler", - "//model:thumbnail_java_proto_lite", + "//model/src/main/proto:thumbnail_java_proto_lite", "//third_party:androidx_annotation_annotation", "//third_party:androidx_constraintlayout_constraintlayout", "//third_party:androidx_lifecycle_lifecycle-livedata-core", @@ -735,7 +735,7 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/oppialogger:startup_listener", "//domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions:logger_module", "//domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader:worker_module", - "//model:arguments_java_proto_lite", + "//model/src/main/proto:arguments_java_proto_lite", "//app/src/main/java/org/oppia/android/app/testing/activity:test_activity", "//third_party:androidx_databinding_databinding-adapters", "//third_party:androidx_databinding_databinding-common", diff --git a/app/src/main/java/org/oppia/android/app/activity/BUILD.bazel b/app/src/main/java/org/oppia/android/app/activity/BUILD.bazel index 8b959707002..3c09724c453 100644 --- a/app/src/main/java/org/oppia/android/app/activity/BUILD.bazel +++ b/app/src/main/java/org/oppia/android/app/activity/BUILD.bazel @@ -75,6 +75,6 @@ kt_android_library( "//app:app_visibility", ], deps = [ - "//model:profile_java_proto_lite", + "//model/src/main/proto:profile_java_proto_lite", ], ) diff --git a/config/config_proto_assets.bzl b/config/config_proto_assets.bzl index f3073c33163..c45099babd2 100644 --- a/config/config_proto_assets.bzl +++ b/config/config_proto_assets.bzl @@ -23,8 +23,8 @@ def generate_supported_languages_configuration_from_text_proto( names = [supported_language_text_proto_file_name], proto_dep_name = "languages", proto_type_name = "SupportedLanguages", - name_prefix = name, + name_prefix = "supported_languages", asset_dir = "languages", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) diff --git a/config/src/java/org/oppia/android/config/BUILD.bazel b/config/src/java/org/oppia/android/config/BUILD.bazel index 36079416378..8cc9ae4591c 100644 --- a/config/src/java/org/oppia/android/config/BUILD.bazel +++ b/config/src/java/org/oppia/android/config/BUILD.bazel @@ -10,7 +10,7 @@ _SUPPORTED_LANGUAGES_CONFIG_ASSETS = generate_proto_binary_assets( asset_dir = "languages", name_prefix = "supported_languages_config_assets", names = ["supported_languages"], - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_dep_name = "languages", proto_package = "model", proto_type_name = "SupportedLanguages", @@ -21,7 +21,7 @@ _SUPPORTED_REGIONS_CONFIG_ASSETS = generate_proto_binary_assets( asset_dir = "languages", name_prefix = "supported_regions_config_assets", names = ["supported_regions"], - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_dep_name = "languages", proto_package = "model", proto_type_name = "SupportedRegions", diff --git a/data/BUILD.bazel b/data/BUILD.bazel index 3dc0dde487c..18fe1fd9127 100644 --- a/data/BUILD.bazel +++ b/data/BUILD.bazel @@ -14,7 +14,7 @@ TEST_DEPS = [ "//data/src/main/java/org/oppia/android/data/backends/gae:prod_module", "//data/src/main/java/org/oppia/android/data/backends/gae/model", "//data/src/main/java/org/oppia/android/data/persistence:cache_store", - "//model:test_models", + "//model/src/main/proto:test_models", "//testing", "//testing/src/main/java/org/oppia/android/testing/network", "//testing/src/main/java/org/oppia/android/testing/network:test_module", diff --git a/data/src/main/java/org/oppia/android/data/backends/gae/BUILD.bazel b/data/src/main/java/org/oppia/android/data/backends/gae/BUILD.bazel index 991bb9cb694..c5859673b0f 100644 --- a/data/src/main/java/org/oppia/android/data/backends/gae/BUILD.bazel +++ b/data/src/main/java/org/oppia/android/data/backends/gae/BUILD.bazel @@ -17,7 +17,7 @@ kt_android_library( deps = [ ":constants", ":network_config_annotations", - "//model:arguments_java_proto_lite", + "//model/src/main/proto:arguments_java_proto_lite", "//third_party:com_squareup_okhttp3_okhttp", "//third_party:javax_inject_javax_inject", "//utility/src/main/java/org/oppia/android/util/extensions:context_extensions", diff --git a/data/src/main/java/org/oppia/android/data/persistence/BUILD.bazel b/data/src/main/java/org/oppia/android/data/persistence/BUILD.bazel index 0ead7ddfd08..db67d9fb798 100644 --- a/data/src/main/java/org/oppia/android/data/persistence/BUILD.bazel +++ b/data/src/main/java/org/oppia/android/data/persistence/BUILD.bazel @@ -12,7 +12,7 @@ kt_android_library( visibility = ["//:oppia_api_visibility"], deps = [ ":dagger", - "//model:profile_java_proto_lite", + "//model/src/main/proto:profile_java_proto_lite", "//utility", "//utility/src/main/java/org/oppia/android/util/data:async_data_subscription_manager", "//utility/src/main/java/org/oppia/android/util/data:async_result", diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index 0edd663c6e5..503c519682e 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -112,11 +112,11 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/util:asset", "//domain/src/main/java/org/oppia/android/domain/util:extensions", "//domain/src/main/java/org/oppia/android/domain/util:retriever", - "//model:exploration_checkpoint_java_proto_lite", - "//model:onboarding_java_proto_lite", - "//model:platform_parameter_java_proto_lite", - "//model:question_java_proto_lite", - "//model:topic_java_proto_lite", + "//model/src/main/proto:exploration_checkpoint_java_proto_lite", + "//model/src/main/proto:onboarding_java_proto_lite", + "//model/src/main/proto:platform_parameter_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", + "//model/src/main/proto:topic_java_proto_lite", "//third_party:androidx_work_work-runtime-ktx", "//utility/src/main/java/org/oppia/android/util/caching:topic_list_to_cache", "//utility/src/main/java/org/oppia/android/util/data:data_providers", @@ -149,7 +149,7 @@ kt_android_library( "src/test/java/org/oppia/android/domain/classify/InteractionObjectTestBuilder.kt", ], deps = [ - "//model:question_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", ], ) diff --git a/domain/domain_assets.bzl b/domain/domain_assets.bzl index dff28e34f9e..89a3ae008a3 100644 --- a/domain/domain_assets.bzl +++ b/domain/domain_assets.bzl @@ -32,53 +32,53 @@ def generate_assets_list_from_text_protos( names = topic_list_file_names, proto_dep_name = "topic", proto_type_name = "TopicIdList", - name_prefix = name, + name_prefix = "topic_id_list", asset_dir = "src/main/assets", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) + generate_proto_binary_assets( name = name, names = topic_file_names, proto_dep_name = "topic", proto_type_name = "TopicRecord", - name_prefix = name, + name_prefix = "topic_record", asset_dir = "src/main/assets", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) + generate_proto_binary_assets( name = name, names = subtopic_file_names, proto_dep_name = "topic", proto_type_name = "SubtopicRecord", - name_prefix = name, + name_prefix = "subtopic_record", asset_dir = "src/main/assets", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) + generate_proto_binary_assets( name = name, names = story_file_names, proto_dep_name = "topic", proto_type_name = "StoryRecord", - name_prefix = name, + name_prefix = "story_record", asset_dir = "src/main/assets", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) + generate_proto_binary_assets( name = name, names = skills_file_names, proto_dep_name = "topic", proto_type_name = "ConceptCardList", - name_prefix = name, + name_prefix = "concept_card_list", asset_dir = "src/main/assets", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) + generate_proto_binary_assets( name = name, names = exploration_file_names, proto_dep_name = "exploration", proto_type_name = "Exploration", - name_prefix = name, + name_prefix = "exploration", asset_dir = "src/main/assets", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) diff --git a/domain/src/main/java/org/oppia/android/domain/audio/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/audio/BUILD.bazel index 440876b4862..c7141fc8c91 100644 --- a/domain/src/main/java/org/oppia/android/domain/audio/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/audio/BUILD.bazel @@ -33,7 +33,7 @@ kt_android_library( deps = [ "//data/src/main/java/org/oppia/android/data/persistence:cache_store", "//domain/src/main/java/org/oppia/android/domain/oppialogger:oppia_logger", - "//model:topic_java_proto_lite", + "//model/src/main/proto:topic_java_proto_lite", "//third_party:javax_inject_javax_inject", "//utility/src/main/java/org/oppia/android/util/data:data_provider", ], diff --git a/domain/src/main/java/org/oppia/android/domain/locale/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/locale/BUILD.bazel index afc2f8b9fca..a62092b664e 100644 --- a/domain/src/main/java/org/oppia/android/domain/locale/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/locale/BUILD.bazel @@ -17,7 +17,7 @@ kt_android_library( ":display_locale_impl", ":language_config_retriever", "//domain/src/main/java/org/oppia/android/domain/oppialogger:oppia_logger", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/data:async_result", "//utility/src/main/java/org/oppia/android/util/data:data_provider", "//utility/src/main/java/org/oppia/android/util/locale:oppia_locale", @@ -64,7 +64,7 @@ kt_android_library( "//domain:domain_testing_visibility", ], deps = [ - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/locale:oppia_locale", ], ) @@ -80,7 +80,7 @@ kt_android_library( deps = [ ":dagger", "//config/src/java/org/oppia/android/config:languages_config", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/caching:annotations", "//utility/src/main/java/org/oppia/android/util/caching:asset_repository", ], diff --git a/domain/src/main/java/org/oppia/android/domain/onboarding/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/onboarding/BUILD.bazel index eb14efb6c0a..f3ba2025c05 100644 --- a/domain/src/main/java/org/oppia/android/domain/onboarding/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/onboarding/BUILD.bazel @@ -15,7 +15,7 @@ kt_android_library( ":exploration_meta_data_retriever", "//data/src/main/java/org/oppia/android/data/persistence:cache_store", "//domain/src/main/java/org/oppia/android/domain/oppialogger:oppia_logger", - "//model:onboarding_java_proto_lite", + "//model/src/main/proto:onboarding_java_proto_lite", "//third_party:javax_inject_javax_inject", "//utility/src/main/java/org/oppia/android/util/data:data_provider", "//utility/src/main/java/org/oppia/android/util/data:data_providers", diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel index e01fb432ed4..38af73c4546 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel @@ -13,7 +13,7 @@ kt_android_library( visibility = ["//domain:__subpackages__"], deps = [ "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:controller", - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/logging:console_logger", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/BUILD.bazel index 320025217d9..0166ef20c36 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/BUILD.bazel @@ -13,7 +13,7 @@ kt_android_library( deps = [ "//data/src/main/java/org/oppia/android/data/persistence:cache_store", "//domain/src/main/java/org/oppia/android/domain/oppialogger:storage_module", - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/data:data_provider", "//utility/src/main/java/org/oppia/android/util/logging:console_logger", "//utility/src/main/java/org/oppia/android/util/logging:event_logger", diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions/BUILD.bazel index 0cd3f1b4a2b..af13183d30c 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions/BUILD.bazel @@ -14,7 +14,7 @@ kt_android_library( deps = [ "//data/src/main/java/org/oppia/android/data/persistence:cache_store", "//domain/src/main/java/org/oppia/android/domain/oppialogger:storage_module", - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/data:data_provider", "//utility/src/main/java/org/oppia/android/util/logging:console_logger", "//utility/src/main/java/org/oppia/android/util/logging:exception_logger", diff --git a/domain/src/main/java/org/oppia/android/domain/state/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/state/BUILD.bazel index 5568e6221f2..aa32d0a3110 100644 --- a/domain/src/main/java/org/oppia/android/domain/state/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/state/BUILD.bazel @@ -11,7 +11,7 @@ kt_android_library( ], visibility = ["//:oppia_api_visibility"], deps = [ - "//model:exploration_checkpoint_java_proto_lite", + "//model/src/main/proto:exploration_checkpoint_java_proto_lite", ], ) @@ -22,7 +22,7 @@ kt_android_library( ], visibility = ["//:oppia_api_visibility"], deps = [ - "//model:exploration_checkpoint_java_proto_lite", + "//model/src/main/proto:exploration_checkpoint_java_proto_lite", ], ) @@ -33,7 +33,7 @@ kt_android_library( ], visibility = ["//:oppia_api_visibility"], deps = [ - "//model:exploration_java_proto_lite", - "//model:question_java_proto_lite", + "//model/src/main/proto:exploration_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/translation/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/translation/BUILD.bazel index 4096a7f922f..700a8b0ad15 100644 --- a/domain/src/main/java/org/oppia/android/domain/translation/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/translation/BUILD.bazel @@ -13,12 +13,12 @@ kt_android_library( visibility = ["//:oppia_api_visibility"], deps = [ "//domain/src/main/java/org/oppia/android/domain/locale:locale_controller", - "//model:interaction_object_java_proto_lite", - "//model:languages_java_proto_lite", - "//model:profile_java_proto_lite", - "//model:subtitled_html_java_proto_lite", - "//model:subtitled_unicode_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", + "//model/src/main/proto:profile_java_proto_lite", + "//model/src/main/proto:subtitled_html_java_proto_lite", + "//model/src/main/proto:subtitled_unicode_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/data:async_result", "//utility/src/main/java/org/oppia/android/util/data:data_provider", "//utility/src/main/java/org/oppia/android/util/data:data_providers", 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 923a0a54e53..26dc7fb7027 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 @@ -31,7 +31,7 @@ kt_android_library( ], visibility = ["//domain:__subpackages__"], deps = [ - "//model:question_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", "//third_party:androidx_work_work-runtime-ktx", ], ) @@ -44,7 +44,7 @@ kt_android_library( visibility = ["//domain:__subpackages__"], deps = [ ":extensions", - "//model:question_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) diff --git a/domain/src/test/java/org/oppia/android/domain/locale/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/locale/BUILD.bazel index 6d992879342..b1a8a187562 100644 --- a/domain/src/test/java/org/oppia/android/domain/locale/BUILD.bazel +++ b/domain/src/test/java/org/oppia/android/domain/locale/BUILD.bazel @@ -34,7 +34,7 @@ oppia_android_test( deps = [ ":dagger", "//domain/src/main/java/org/oppia/android/domain/locale:content_locale_impl", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_extensions_truth-liteproto-extension", "//third_party:com_google_truth_truth", @@ -54,7 +54,7 @@ oppia_android_test( ":dagger", "//domain:test_resources", "//domain/src/main/java/org/oppia/android/domain/locale:display_locale_impl", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//testing", "//testing/src/main/java/org/oppia/android/testing/time:test_module", "//third_party:androidx_test_ext_junit", @@ -121,7 +121,7 @@ oppia_android_test( deps = [ ":dagger", "//domain/src/main/java/org/oppia/android/domain/locale:locale_controller", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//testing", "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", diff --git a/domain/src/test/java/org/oppia/android/domain/translation/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/translation/BUILD.bazel index 0877354b02c..d4733f05076 100644 --- a/domain/src/test/java/org/oppia/android/domain/translation/BUILD.bazel +++ b/domain/src/test/java/org/oppia/android/domain/translation/BUILD.bazel @@ -15,7 +15,7 @@ oppia_android_test( ":dagger", "//domain/src/main/java/org/oppia/android/domain/locale:locale_controller", "//domain/src/main/java/org/oppia/android/domain/translation:translation_controller", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//testing", "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", diff --git a/model/BUILD.bazel b/model/BUILD.bazel index d64d7e2a340..1755f53361e 100644 --- a/model/BUILD.bazel +++ b/model/BUILD.bazel @@ -1,270 +1,3 @@ -# TODO(#1532): Rename file to 'BUILD' post-Gradle. """ -This library contains all protos used in the app and is a dependency for all other modules. -In Bazel, proto files are built using the proto_library() and java_lite_proto_library() rules. -The proto_library() rule creates a proto file library to be used in multiple languages. -The java_lite_proto_library() rule takes in a proto_library target and generates java code. +TODO: add docs """ - -load("@rules_java//java:defs.bzl", "java_lite_proto_library") -load("@rules_proto//proto:defs.bzl", "proto_library") -load("//model:src/main/proto/format_import_proto_library.bzl", "format_import_proto_library") - -# NOTE TO DEVELOPERS: When adding new protos, each proto will need to have both a proto_library -# and java_lite_proto_library. See the examples below for context. Further, once the proto lite -# library is added, it should be included in the exports list in the model library at the -# bottom of this file so that other parts of the app get access to it. If protos import other -# protos, they need to use format_import_proto_library (again, see examples below for how to do -# this). -# -# For example, if adding a new proto file called 'important_structure.proto', add these: -# proto_library( -# name = "important_structure_proto", -# srcs = ["src/main/proto/important_structure.proto"], -# ) -# -# java_lite_proto_library( -# name = "important_structure_java_proto_lite", -# deps = [":important_structure_proto"], -# ) -# -# And change the 'model' library at the bottom of the file, e.g.: -# android_library( -# name = "model", -# exports = [ -# ... -# ":important_structure_java_proto_lite", -# ... -# ], -# ... -# ) - -proto_library( - name = "arguments_proto", - srcs = ["src/main/proto/arguments.proto"], -) - -java_lite_proto_library( - name = "arguments_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":arguments_proto"], -) - -proto_library( - name = "event_logger_proto", - srcs = ["src/main/proto/oppia_logger.proto"], -) - -java_lite_proto_library( - name = "event_logger_java_proto_lite", - visibility = ["//visibility:public"], - deps = [":event_logger_proto"], -) - -format_import_proto_library( - name = "exploration_checkpoint", - src = "src/main/proto/exploration_checkpoint.proto", - deps = [":exploration_proto"], -) - -java_lite_proto_library( - name = "exploration_checkpoint_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":exploration_checkpoint_proto"], -) - -proto_library( - name = "interaction_object_proto", - srcs = ["src/main/proto/interaction_object.proto"], -) - -java_lite_proto_library( - name = "interaction_object_java_proto_lite", - visibility = ["//visibility:public"], - deps = [":interaction_object_proto"], -) - -proto_library( - name = "languages_proto", - srcs = ["src/main/proto/languages.proto"], - visibility = ["//visibility:public"], -) - -java_lite_proto_library( - name = "languages_java_proto_lite", - visibility = ["//visibility:public"], - deps = [":languages_proto"], -) - -proto_library( - name = "onboarding_proto", - srcs = ["src/main/proto/onboarding.proto"], -) - -java_lite_proto_library( - name = "onboarding_java_proto_lite", - visibility = ["//visibility:public"], - deps = [":onboarding_proto"], -) - -proto_library( - name = "profile_proto", - srcs = ["src/main/proto/profile.proto"], -) - -java_lite_proto_library( - name = "profile_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":profile_proto"], -) - -proto_library( - name = "subtitled_html_proto", - srcs = ["src/main/proto/subtitled_html.proto"], -) - -java_lite_proto_library( - name = "subtitled_html_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":subtitled_html_proto"], -) - -proto_library( - name = "subtitled_unicode_proto", - srcs = ["src/main/proto/subtitled_unicode.proto"], -) - -java_lite_proto_library( - name = "subtitled_unicode_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":subtitled_unicode_proto"], -) - -proto_library( - name = "test_proto", - srcs = ["src/main/proto/test.proto"], -) - -java_lite_proto_library( - name = "test_java_proto_lite", - deps = [":test_proto"], -) - -proto_library( - name = "thumbnail_proto", - srcs = ["src/main/proto/thumbnail.proto"], -) - -java_lite_proto_library( - name = "thumbnail_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":thumbnail_proto"], -) - -proto_library( - name = "translation_proto", - srcs = ["src/main/proto/translation.proto"], -) - -java_lite_proto_library( - name = "translation_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":translation_proto"], -) - -proto_library( - name = "voiceover_proto", - srcs = ["src/main/proto/voiceover.proto"], -) - -java_lite_proto_library( - name = "voiceover_java_proto_lite", - deps = [":voiceover_proto"], -) - -format_import_proto_library( - name = "feedback_reporting", - src = "src/main/proto/feedback_reporting.proto", - deps = [ - ":profile_proto", - ], -) - -java_lite_proto_library( - name = "feedback_reporting_java_proto_lite", - deps = [":feedback_reporting_proto"], -) - -format_import_proto_library( - name = "question", - src = "src/main/proto/question.proto", - deps = [ - ":exploration_proto", - ":subtitled_html_proto", - ":subtitled_unicode_proto", - ], -) - -java_lite_proto_library( - name = "question_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":question_proto"], -) - -format_import_proto_library( - name = "topic", - src = "src/main/proto/topic.proto", - visibility = ["//visibility:public"], - deps = [ - ":subtitled_html_proto", - ":subtitled_unicode_proto", - ":thumbnail_proto", - ":translation_proto", - ":voiceover_proto", - ], -) - -java_lite_proto_library( - name = "topic_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":topic_proto"], -) - -format_import_proto_library( - name = "exploration", - src = "src/main/proto/exploration.proto", - visibility = ["//visibility:public"], - deps = [ - ":interaction_object_proto", - ":subtitled_html_proto", - ":subtitled_unicode_proto", - ":translation_proto", - ":voiceover_proto", - ], -) - -java_lite_proto_library( - name = "exploration_java_proto_lite", - visibility = ["//visibility:public"], - deps = [":exploration_proto"], -) - -format_import_proto_library( - name = "platform_parameter", - src = "src/main/proto/platform_parameter.proto", -) - -java_lite_proto_library( - name = "platform_parameter_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":platform_parameter_proto"], -) - -android_library( - name = "test_models", - testonly = True, - visibility = ["//visibility:public"], - exports = [ - ":test_java_proto_lite", - ], -) diff --git a/model/oppia_proto_library.bzl b/model/oppia_proto_library.bzl new file mode 100644 index 00000000000..8f6ac753135 --- /dev/null +++ b/model/oppia_proto_library.bzl @@ -0,0 +1,19 @@ +""" +TODO: add docs +""" + +load("@rules_proto//proto:defs.bzl", "proto_library") + +# TODO: add regex check +# TODO: add TODO to remove +# TODO: maybe close format proto issue with this PR? + +def oppia_proto_library(name, strip_import_prefix = "", **kwargs): + """ + TODO: add docs + """ + proto_library( + name = name, + strip_import_prefix = strip_import_prefix, + **kwargs + ) diff --git a/model/src/main/proto/BUILD.bazel b/model/src/main/proto/BUILD.bazel new file mode 100644 index 00000000000..90da6e48b77 --- /dev/null +++ b/model/src/main/proto/BUILD.bazel @@ -0,0 +1,257 @@ +# TODO(#1532): Rename file to 'BUILD' post-Gradle. +""" +This library contains all protos used in the app and is a dependency for all other modules. +In Bazel, proto files are built using the oppia_proto_library() and java_lite_proto_library() rules. +The oppia_proto_library() rule creates a proto file library to be used in multiple languages. +The java_lite_proto_library() rule takes in a proto_library target and generates java code. +""" + +load("@rules_java//java:defs.bzl", "java_lite_proto_library") +load("//model:oppia_proto_library.bzl", "oppia_proto_library") + +# NOTE TO DEVELOPERS: When adding new protos, each proto will need to have both a proto_library +# and java_lite_proto_library. +# +# For example, if adding a new proto file called 'important_structure.proto', add these: +# oppia_proto_library( +# name = "important_structure_proto", +# srcs = ["src/main/proto/important_structure.proto"], +# ) +# +# java_lite_proto_library( +# name = "important_structure_java_proto_lite", +# deps = [":important_structure_proto"], +# ) + +oppia_proto_library( + name = "arguments_proto", + srcs = ["arguments.proto"], +) + +java_lite_proto_library( + name = "arguments_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":arguments_proto"], +) + +oppia_proto_library( + name = "event_logger_proto", + srcs = ["oppia_logger.proto"], +) + +java_lite_proto_library( + name = "event_logger_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":event_logger_proto"], +) + +oppia_proto_library( + name = "exploration_checkpoint_proto", + srcs = ["exploration_checkpoint.proto"], + deps = [":exploration_proto"], +) + +java_lite_proto_library( + name = "exploration_checkpoint_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":exploration_checkpoint_proto"], +) + +oppia_proto_library( + name = "interaction_object_proto", + srcs = ["interaction_object.proto"], + visibility = ["//:oppia_api_visibility"], +) + +java_lite_proto_library( + name = "interaction_object_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":interaction_object_proto"], +) + +oppia_proto_library( + name = "languages_proto", + srcs = ["languages.proto"], + visibility = ["//:oppia_api_visibility"], +) + +java_lite_proto_library( + name = "languages_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":languages_proto"], +) + +oppia_proto_library( + name = "onboarding_proto", + srcs = ["onboarding.proto"], +) + +java_lite_proto_library( + name = "onboarding_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":onboarding_proto"], +) + +oppia_proto_library( + name = "profile_proto", + srcs = ["profile.proto"], +) + +java_lite_proto_library( + name = "profile_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":profile_proto"], +) + +oppia_proto_library( + name = "subtitled_html_proto", + srcs = ["subtitled_html.proto"], + visibility = ["//:oppia_api_visibility"], +) + +java_lite_proto_library( + name = "subtitled_html_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":subtitled_html_proto"], +) + +oppia_proto_library( + name = "subtitled_unicode_proto", + srcs = ["subtitled_unicode.proto"], + visibility = ["//:oppia_api_visibility"], +) + +java_lite_proto_library( + name = "subtitled_unicode_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":subtitled_unicode_proto"], +) + +oppia_proto_library( + name = "test_proto", + srcs = ["test.proto"], +) + +java_lite_proto_library( + name = "test_java_proto_lite", + deps = [":test_proto"], +) + +oppia_proto_library( + name = "thumbnail_proto", + srcs = ["thumbnail.proto"], +) + +java_lite_proto_library( + name = "thumbnail_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":thumbnail_proto"], +) + +oppia_proto_library( + name = "translation_proto", + srcs = ["translation.proto"], + visibility = ["//:oppia_api_visibility"], +) + +java_lite_proto_library( + name = "translation_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":translation_proto"], +) + +oppia_proto_library( + name = "voiceover_proto", + srcs = ["voiceover.proto"], + visibility = ["//:oppia_api_visibility"], +) + +java_lite_proto_library( + name = "voiceover_java_proto_lite", + deps = [":voiceover_proto"], +) + +oppia_proto_library( + name = "feedback_reporting_proto", + srcs = ["feedback_reporting.proto"], + deps = [":profile_proto"], +) + +java_lite_proto_library( + name = "feedback_reporting_java_proto_lite", + deps = [":feedback_reporting_proto"], +) + +oppia_proto_library( + name = "question_proto", + srcs = ["question.proto"], + deps = [ + ":exploration_proto", + ":subtitled_html_proto", + ":subtitled_unicode_proto", + ], +) + +java_lite_proto_library( + name = "question_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":question_proto"], +) + +oppia_proto_library( + name = "topic_proto", + srcs = ["topic.proto"], + visibility = ["//:oppia_api_visibility"], + deps = [ + ":subtitled_html_proto", + ":subtitled_unicode_proto", + ":thumbnail_proto", + ":translation_proto", + ":voiceover_proto", + ], +) + +java_lite_proto_library( + name = "topic_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":topic_proto"], +) + +oppia_proto_library( + name = "exploration_proto", + srcs = ["exploration.proto"], + visibility = ["//:oppia_api_visibility"], + deps = [ + ":interaction_object_proto", + ":subtitled_html_proto", + ":subtitled_unicode_proto", + ":translation_proto", + ":voiceover_proto", + ], +) + +java_lite_proto_library( + name = "exploration_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":exploration_proto"], +) + +oppia_proto_library( + name = "platform_parameter_proto", + srcs = ["platform_parameter.proto"], +) + +java_lite_proto_library( + name = "platform_parameter_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":platform_parameter_proto"], +) + +android_library( + name = "test_models", + testonly = True, + visibility = ["//:oppia_api_visibility"], + exports = [ + ":test_java_proto_lite", + ], +) diff --git a/model/src/main/proto/format_import_proto_library.bzl b/model/src/main/proto/format_import_proto_library.bzl deleted file mode 100644 index 77ab61f0fdf..00000000000 --- a/model/src/main/proto/format_import_proto_library.bzl +++ /dev/null @@ -1,44 +0,0 @@ -""" -Container for macros to fix proto files. -""" - -load("@rules_proto//proto:defs.bzl", "proto_library") - -def format_import_proto_library(name, src, deps = [], **kwargs): - """ - Creates a new proto library with corrected imports. - - This macro exists as a way to build proto files that contain import statements in both Gradle - and Bazel. This macro formats the src file's import statements to contain a full path to the - file in order for Bazel to properly locate file. - - Args: - name: str. The name of the .proto file without the '.proto' suffix. This will be the root for - the name of the proto library created. Ex: If name = 'topic', then the src file is - 'topic.proto' and the proto library created will be named 'topic_proto'. - src: str. The name of the .proto file to be built into a proto_library. - deps: list of str. The list of dependencies needed to build the src file. This list will - contain all of the proto_library targets for the files imported into src. - **kwargs: additional parameters passed in. - """ - - # TODO(#1543): Ensure this function works on Windows systems. - # TODO(#1617): Remove genrules post-gradle - native.genrule( - name = name, - srcs = [src], - outs = ["processed_" + src], - cmd = """ - cat $< | - sed 's/import "/import "model\\/src\\/main\\/proto\\//g' | - sed 's/"model\\/src\\/main\\/proto\\/exploration/"model\\/processed_src\\/main\\/proto\\/exploration/g' | - sed 's/"model\\/src\\/main\\/proto\\/topic/"model\\/processed_src\\/main\\/proto\\/topic/g' | - sed 's/"model\\/src\\/main\\/proto\\/question/"model\\/processed_src\\/main\\/proto\\/question/g' > $@ - """, - ) - proto_library( - name = name + "_proto", - srcs = ["processed_" + src], - deps = deps, - **kwargs - ) diff --git a/model/text_proto_assets.bzl b/model/text_proto_assets.bzl index 326dd00933a..ace61c6a81b 100644 --- a/model/text_proto_assets.bzl +++ b/model/text_proto_assets.bzl @@ -29,9 +29,11 @@ def _gen_binary_proto_from_text_impl(ctx): # proto to binary, and expected stdin/stdout configurations. Note that the actual proto files # are passed to the compiler since it requires them in order to transcode the text proto file. command_path = ctx.executable._protoc_tool.path + proto_directory_path_args = ["--proto_path=%s" % file.dirname for file in input_proto_files] + proto_file_names = [file.basename for file in input_proto_files] arguments = [command_path] + [ "--encode %s" % ctx.attr.proto_type_name, - ] + [file.path for file in input_proto_files] + [ + ] + proto_directory_path_args + proto_file_names + [ "< %s" % input_file, "> %s" % output_file.path, ] diff --git a/scripts/script_assets.bzl b/scripts/script_assets.bzl index b5c3fb389e4..363455fb553 100644 --- a/scripts/script_assets.bzl +++ b/scripts/script_assets.bzl @@ -24,7 +24,7 @@ def generate_regex_assets_list_from_text_protos( names = filepath_pattern_validation_file_names, proto_dep_name = "filename_pattern_validation_checks", proto_type_name = "FilenameChecks", - name_prefix = name, + name_prefix = "filename_checks", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", @@ -33,7 +33,7 @@ def generate_regex_assets_list_from_text_protos( names = file_content_validation_file_names, proto_dep_name = "file_content_validation_checks", proto_type_name = "FileContentChecks", - name_prefix = name, + name_prefix = "file_content_checks", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", @@ -57,7 +57,7 @@ def generate_test_file_assets_list_from_text_protos( names = test_file_exemptions_name, proto_dep_name = "script_exemptions", proto_type_name = "TestFileExemptions", - name_prefix = name, + name_prefix = "test_file_exemptions", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", @@ -82,7 +82,7 @@ def generate_maven_assets_list_from_text_protos( names = maven_dependency_filenames, proto_dep_name = "maven_dependencies", proto_type_name = "MavenDependencyList", - name_prefix = name, + name_prefix = "maven_dependency_list", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", @@ -107,7 +107,7 @@ def generate_accessibility_label_assets_list_from_text_protos( names = accessibility_label_exemptions_name, proto_dep_name = "script_exemptions", proto_type_name = "AccessibilityLabelExemptions", - name_prefix = name, + name_prefix = "accessibility_label_exemptions", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", @@ -131,7 +131,7 @@ def generate_kdoc_validity_assets_list_from_text_protos( names = kdoc_validity_exemptions_name, proto_dep_name = "script_exemptions", proto_type_name = "KdocValidityExemptions", - name_prefix = name, + name_prefix = "kdoc_validity_exemptions", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", @@ -155,7 +155,7 @@ def generate_todo_assets_list_from_text_protos( names = todo_exemptions_name, proto_dep_name = "script_exemptions", proto_type_name = "TodoOpenExemptions", - name_prefix = name, + name_prefix = "todo_open_exemptions", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", diff --git a/scripts/src/javatests/org/oppia/android/scripts/common/BUILD.bazel b/scripts/src/javatests/org/oppia/android/scripts/common/BUILD.bazel index b4559459055..e47fc741ab6 100644 --- a/scripts/src/javatests/org/oppia/android/scripts/common/BUILD.bazel +++ b/scripts/src/javatests/org/oppia/android/scripts/common/BUILD.bazel @@ -43,7 +43,7 @@ kt_jvm_test( name = "ProtoStringEncoderTest", srcs = ["ProtoStringEncoderTest.kt"], deps = [ - "//model:test_models", + "//model/src/main/proto:test_models", "//scripts/src/java/org/oppia/android/scripts/common:proto_string_encoder", "//testing:assertion_helpers", "//third_party:com_google_truth_extensions_truth-liteproto-extension", diff --git a/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel index e2c54937b89..e8d62702d99 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel @@ -25,7 +25,7 @@ kt_android_library( ":define_app_language_locale_context", "//domain/src/main/java/org/oppia/android/domain/locale:locale_application_injector", "//domain/src/main/java/org/oppia/android/domain/locale:locale_application_injector_provider", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//third_party:androidx_test_core", "//third_party:junit_junit", ], diff --git a/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel b/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel index 4b84736203b..bfc5a1ab0ac 100644 --- a/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel +++ b/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel @@ -16,7 +16,7 @@ oppia_android_test( ":dagger", "//domain/src/main/java/org/oppia/android/domain/locale:locale_controller", "//domain/src/main/java/org/oppia/android/domain/translation:translation_controller", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//testing", "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", diff --git a/utility/BUILD.bazel b/utility/BUILD.bazel index 98a35c2ae8f..99cc8627c08 100644 --- a/utility/BUILD.bazel +++ b/utility/BUILD.bazel @@ -54,8 +54,8 @@ kt_android_library( ":resources", "//app:crashlytics", "//app:crashlytics_deps", - "//model:event_logger_java_proto_lite", - "//model:platform_parameter_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", + "//model/src/main/proto:platform_parameter_java_proto_lite", "//third_party:androidx_appcompat_appcompat", "//third_party:androidx_room_room-runtime", "//third_party:androidx_work_work-runtime", @@ -91,7 +91,7 @@ TEST_DEPS = [ ":utility", "//app:crashlytics", "//app:crashlytics_deps", - "//model:test_models", + "//model/src/main/proto:test_models", "//testing", "//testing/src/main/java/org/oppia/android/testing/mockito", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", diff --git a/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel index 1522fe1a28a..0c57bde04d0 100644 --- a/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel @@ -36,7 +36,7 @@ kt_android_library( visibility = ["//:oppia_api_visibility"], deps = [ ":oppia_locale_context_extensions", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//third_party:androidx_annotation_annotation", ], ) @@ -63,7 +63,7 @@ kt_android_library( "OppiaLocaleContextExtensions.kt", ], deps = [ - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", ], ) diff --git a/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel index 3ea1f8bbe89..21b2fa9154c 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel @@ -39,7 +39,7 @@ kt_android_library( ], visibility = ["//:oppia_api_visibility"], deps = [ - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", ], ) @@ -50,7 +50,7 @@ kt_android_library( ], visibility = ["//:oppia_api_visibility"], deps = [ - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", ], ) diff --git a/utility/src/main/java/org/oppia/android/util/logging/firebase/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/logging/firebase/BUILD.bazel index 3a69f05bcf2..7c5e92b2d2d 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/firebase/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/logging/firebase/BUILD.bazel @@ -23,7 +23,7 @@ kt_android_library( "FirebaseLogUploader.kt", ], deps = [ - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", "//third_party:androidx_work_work-runtime", "//third_party:androidx_work_work-runtime-ktx", "//third_party:com_google_firebase_firebase-analytics", @@ -62,7 +62,7 @@ kt_android_library( "//app:__pkg__", ], deps = [ - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", "//third_party:javax_inject_javax_inject", "//utility/src/main/java/org/oppia/android/util/logging:event_logger", ], diff --git a/utility/src/test/java/org/oppia/android/util/caching/testing/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/caching/testing/BUILD.bazel index a4187f0dede..f9cf0e1a190 100644 --- a/utility/src/test/java/org/oppia/android/util/caching/testing/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/caching/testing/BUILD.bazel @@ -31,7 +31,7 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ ":dagger", - "//model:test_models", + "//model/src/main/proto:test_models", "//testing", "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_extensions_truth-liteproto-extension", diff --git a/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel index 43c6ea60c0c..075455e6297 100644 --- a/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel @@ -87,7 +87,7 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ ":dagger", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_extensions_truth-liteproto-extension", "//third_party:junit_junit", From fe73a2f8083dbd48ab207068a7931ff4733bd9f4 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 17:34:44 -0800 Subject: [PATCH 002/134] Introduce math.proto & refactor math extensions. Much of this is copied from #2173. --- ...atioExpressionInputInteractionViewModel.kt | 2 +- domain/BUILD.bazel | 1 + ...AndInSimplestFormRuleClassifierProvider.kt | 6 +++--- ...putIsEquivalentToRuleClassifierProvider.kt | 4 ++-- ...nputIsGreaterThanRuleClassifierProvider.kt | 2 +- ...onInputIsLessThanRuleClassifierProvider.kt | 2 +- ...ithUnitsIsEqualToRuleClassifierProvider.kt | 2 +- ...itsIsEquivalentToRuleClassifierProvider.kt | 4 ++-- ...umericInputEqualsRuleClassifierProvider.kt | 2 +- ...InputIsEquivalentRuleClassifierProvider.kt | 2 +- .../org/oppia/android/domain/util/BUILD.bazel | 4 +--- .../util/InteractionObjectExtensions.kt | 1 + model/src/main/proto/BUILD.bazel | 13 ++++++++++++ model/src/main/proto/interaction_object.proto | 17 ++------------- model/src/main/proto/math.proto | 21 +++++++++++++++++++ utility/BUILD.bazel | 1 + .../org/oppia/android/util/math/BUILD.bazel | 20 ++++++++++++++++++ .../android/util/math}/FloatExtensions.kt | 4 ++-- .../android/util/math}/FractionExtensions.kt | 2 +- .../android/util/math}/RatioExtensions.kt | 2 +- 20 files changed, 77 insertions(+), 35 deletions(-) create mode 100644 model/src/main/proto/math.proto create mode 100644 utility/src/main/java/org/oppia/android/util/math/BUILD.bazel rename {domain/src/main/java/org/oppia/android/domain/util => utility/src/main/java/org/oppia/android/util/math}/FloatExtensions.kt (86%) rename {domain/src/main/java/org/oppia/android/domain/util => utility/src/main/java/org/oppia/android/util/math}/FractionExtensions.kt (96%) rename {domain/src/main/java/org/oppia/android/domain/util => utility/src/main/java/org/oppia/android/util/math}/RatioExtensions.kt (94%) 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/domain/BUILD.bazel b/domain/BUILD.bazel index 503c519682e..cf4b8098c8e 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -124,6 +124,7 @@ kt_android_library( "//utility/src/main/java/org/oppia/android/util/extensions:context_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/profile:directory_management_util", ], 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..598d7a71ad1 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.toFloat +import org.oppia.android.util.math.toSimplestForm import javax.inject.Inject /** 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..0c47a5d0657 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.toFloat import javax.inject.Inject /** 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..a44dedfcfdf 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.toFloat import javax.inject.Inject /** 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..52d1396d9f1 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.toFloat import javax.inject.Inject /** 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..99bad528bb4 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.toFloat import javax.inject.Inject /** 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/util/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel index 26dc7fb7027..7c1dc1e6ce2 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,11 +21,8 @@ kt_android_library( kt_android_library( name = "extensions", srcs = [ - "FloatExtensions.kt", - "FractionExtensions.kt", "InteractionObjectExtensions.kt", "JsonExtensions.kt", - "RatioExtensions.kt", "StringExtensions.kt", "WorkDataExtensions.kt", ], @@ -33,6 +30,7 @@ kt_android_library( 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/model/src/main/proto/BUILD.bazel b/model/src/main/proto/BUILD.bazel index 90da6e48b77..f28661f7341 100644 --- a/model/src/main/proto/BUILD.bazel +++ b/model/src/main/proto/BUILD.bazel @@ -61,6 +61,7 @@ oppia_proto_library( name = "interaction_object_proto", srcs = ["interaction_object.proto"], visibility = ["//:oppia_api_visibility"], + deps = [":math_proto"], ) java_lite_proto_library( @@ -81,6 +82,18 @@ java_lite_proto_library( deps = [":languages_proto"], ) +oppia_proto_library( + name = "math_proto", + srcs = ["math.proto"], + strip_import_prefix = "", +) + +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/utility/BUILD.bazel b/utility/BUILD.bazel index 99cc8627c08..513122f87ef 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/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel new file mode 100644 index 00000000000..4b84961d297 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -0,0 +1,20 @@ +""" +TODO: document +""" + +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", + ], +) 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 96% 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..69b57d5be39 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,4 +1,4 @@ -package org.oppia.android.domain.util +package org.oppia.android.util.math import org.oppia.android.app.model.Fraction 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..123a24e2958 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 From d17e3dc68fb98dc2c0b666194145afa397a5a09a Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 17:49:47 -0800 Subject: [PATCH 003/134] Migrate tests & remove unneeded prefix. --- model/src/main/proto/BUILD.bazel | 1 - .../org/oppia/android/util/math/BUILD.bazel | 22 +++++++++++++++++++ .../android/util/math}/RatioExtensionsTest.kt | 2 +- 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 utility/src/test/java/org/oppia/android/util/math/BUILD.bazel rename {domain/src/test/java/org/oppia/android/domain/util => utility/src/test/java/org/oppia/android/util/math}/RatioExtensionsTest.kt (97%) diff --git a/model/src/main/proto/BUILD.bazel b/model/src/main/proto/BUILD.bazel index f28661f7341..a88f7ec5ed0 100644 --- a/model/src/main/proto/BUILD.bazel +++ b/model/src/main/proto/BUILD.bazel @@ -85,7 +85,6 @@ java_lite_proto_library( oppia_proto_library( name = "math_proto", srcs = ["math.proto"], - strip_import_prefix = "", ) java_lite_proto_library( 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..493d89d66a0 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -0,0 +1,22 @@ +""" +TODO: document +""" + +load("//:oppia_android_test.bzl", "oppia_android_test") + +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/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 From fb61e39bf1f598946e534a2d77a8c456d07f149a Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 17:55:46 -0800 Subject: [PATCH 004/134] Add needed newline. --- .../src/main/java/org/oppia/android/util/math/RatioExtensions.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt index 123a24e2958..83d85e9098c 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt @@ -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 From acab98bd74477a78a55605364df20b4ef029050f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 18:36:04 -0800 Subject: [PATCH 005/134] Some needed Fraction changes. --- ...tToAndInSimplestFormRuleClassifierProvider.kt | 5 +++-- ...nInputIsEquivalentToRuleClassifierProvider.kt | 4 ++-- ...onInputIsGreaterThanRuleClassifierProvider.kt | 4 ++-- ...ctionInputIsLessThanRuleClassifierProvider.kt | 4 ++-- ...hUnitsIsEquivalentToRuleClassifierProvider.kt | 4 ++-- .../android/util/math/FractionExtensions.kt | 16 +++++++++------- 6 files changed, 20 insertions(+), 17 deletions(-) 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 598d7a71ad1..af698227520 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 @@ -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.util.math.approximatelyEquals -import org.oppia.android.util.math.toFloat +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 0c47a5d0657..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 @@ -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.util.math.approximatelyEquals -import org.oppia.android.util.math.toFloat +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 a44dedfcfdf..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.util.math.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 52d1396d9f1..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.util.math.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/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt index 99bad528bb4..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 @@ -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.util.math.approximatelyEquals -import org.oppia.android.util.math.toFloat +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/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index 69b57d5be39..d4a57faf9be 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -3,14 +3,14 @@ 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. */ From 02e930fb825149b24eda7eacfa16fa7fe0b4a131 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 18:46:49 -0800 Subject: [PATCH 006/134] Introduce math expression + equation protos. Also adds testing libraries for both + fractions & reals (new structure). Most of this is copied from #2173. --- model/src/main/proto/math.proto | 77 +++++++ testing/BUILD.bazel | 1 + .../oppia/android/testing/math/BUILD.bazel | 75 +++++++ .../android/testing/math/FractionSubject.kt | 30 +++ .../testing/math/MathEquationSubject.kt | 21 ++ .../testing/math/MathExpressionSubject.kt | 206 ++++++++++++++++++ .../oppia/android/testing/math/RealSubject.kt | 41 ++++ 7 files changed, 451 insertions(+) create mode 100644 testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel create mode 100644 testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index 0288db3148b..7dcbf780370 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -13,9 +13,86 @@ message Fraction { int32 denominator = 4; } +message Real { + oneof real_type { + Fraction rational = 1; + // Represents a decimal value. Technically these can sometimes be rational, but given IEEE-754 + // rounding errors we need to treat these values as irrational and non-factorable. + double irrational = 2; + int32 integer = 3; + } +} + // 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; } + +// Represents a mathematical expression such as 1+2. The only expression currently supported is a +// binary operation. +message MathExpression { + // TODO: document inclusive + int32 parse_start_index = 1; + // TODO: document exclusive + int32 parse_end_index = 2; + + oneof expression_type { + Real constant = 3; + string variable = 4; + MathBinaryOperation binary_operation = 5; + MathUnaryOperation unary_operation = 6; + MathFunctionCall function_call = 7; + MathExpression group = 8; + } +} + +message MathBinaryOperation { + enum Operator { + OPERATOR_UNSPECIFIED = 0; + // Represents adding two values, e.g.: 1+x. + ADD = 1; + // Represents subtracting two values, e.g.: x-2. + SUBTRACT = 2; + // Represents multiplying two values, e.g.: x*y. + MULTIPLY = 3; + // Represents dividing two values, e.g.: 1/x. + DIVIDE = 4; + // Represents taking the exponentiation of one value by another, e.g.: x^2. + EXPONENTIATE = 5; + } + + Operator operator = 1; + MathExpression left_operand = 2; + MathExpression right_operand = 3; + bool is_implicit = 4; +} + +message MathUnaryOperation { + enum Operator { + OPERATOR_UNSPECIFIED = 0; + // Represents negating a value, e.g.: -y. + NEGATE = 1; + // Represents indicating a value as positive, e.g.: +y. + POSITIVE = 2; + } + + Operator operator = 1; + MathExpression operand = 2; +} + +message MathFunctionCall { + enum FunctionType { + FUNCTION_UNSPECIFIED = 0; + SQUARE_ROOT = 1; + } + + FunctionType function_type = 1; + MathExpression argument = 2; +} + +message MathEquation { + MathExpression left_side = 1; + MathExpression right_side = 2; +} diff --git a/testing/BUILD.bazel b/testing/BUILD.bazel index 2d7d6122afc..87734adaad8 100644 --- a/testing/BUILD.bazel +++ b/testing/BUILD.bazel @@ -12,6 +12,7 @@ load("//testing:testing_test.bzl", "testing_test") # globs here to ensure new files added to migrated packages don't accidentally get included in the # top-level module library. MIGRATED_PROD_FILES = glob([ + "src/main/java/org/oppia/android/testing/math/*.kt", "src/main/java/org/oppia/android/testing/mockito/*.kt", "src/main/java/org/oppia/android/testing/networking/*.kt", "src/test/java/org/oppia/android/testing/platformparameter/*.kt", diff --git a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel new file mode 100644 index 00000000000..a1453dd4496 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel @@ -0,0 +1,75 @@ +""" +TODO: document +""" + +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +# TODO(#2747): Move these libraries to be under utility/.../math/testing. + +kt_android_library( + name = "fraction_subject", + testonly = True, + srcs = [ + "FractionSubject.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + ], +) + +kt_android_library( + name = "math_equation_subject", + testonly = True, + srcs = [ + "MathEquationSubject.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":math_expression_subject", + "//model/src/main/proto:math_java_proto_lite", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + ], +) + +kt_android_library( + name = "math_expression_subject", + testonly = True, + srcs = [ + "MathExpressionSubject.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":real_subject", + "//model/src/main/proto:math_java_proto_lite", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + ], +) + +kt_android_library( + name = "real_subject", + testonly = True, + srcs = [ + "RealSubject.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":fraction_subject", + "//model/src/main/proto:math_java_proto_lite", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + ], +) diff --git a/testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt new file mode 100644 index 00000000000..b256d7fd555 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt @@ -0,0 +1,30 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.BooleanSubject +import com.google.common.truth.DoubleSubject +import com.google.common.truth.FailureMetadata +import com.google.common.truth.IntegerSubject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.extensions.proto.LiteProtoSubject +import org.oppia.android.app.model.Fraction +import org.oppia.android.util.math.toDouble + +class FractionSubject( + metadata: FailureMetadata, + private val actual: Fraction +) : LiteProtoSubject(metadata, actual) { + fun hasNegativePropertyThat(): BooleanSubject = assertThat(actual.isNegative) + + fun hasWholeNumberThat(): IntegerSubject = assertThat(actual.wholeNumber) + + fun hasNumeratorThat(): IntegerSubject = assertThat(actual.numerator) + + fun hasDenominatorThat(): IntegerSubject = assertThat(actual.denominator) + + fun evaluatesToRealThat(): DoubleSubject = assertThat(actual.toDouble()) + + companion object { + fun assertThat(actual: Fraction): FractionSubject = assertAbout(::FractionSubject).that(actual) + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt new file mode 100644 index 00000000000..ce24e1e08cc --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt @@ -0,0 +1,21 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.FailureMetadata +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.extensions.proto.LiteProtoSubject +import org.oppia.android.app.model.MathEquation +import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat + +class MathEquationSubject( + metadata: FailureMetadata, + private val actual: MathEquation +) : LiteProtoSubject(metadata, actual) { + fun hasLeftHandSideThat(): MathExpressionSubject = assertThat(actual.leftSide) + + fun hasRightHandSideThat(): MathExpressionSubject = assertThat(actual.rightSide) + + companion object { + fun assertThat(actual: MathEquation): MathEquationSubject = + assertAbout(::MathEquationSubject).that(actual) + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt new file mode 100644 index 00000000000..c9be134e209 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt @@ -0,0 +1,206 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.FailureMetadata +import com.google.common.truth.StringSubject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import com.google.common.truth.extensions.proto.LiteProtoSubject +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathFunctionCall +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.Real +import org.oppia.android.testing.math.RealSubject.Companion.assertThat + +// See: https://kotlinlang.org/docs/type-safe-builders.html. +class MathExpressionSubject( + metadata: FailureMetadata, + private val actual: MathExpression +) : LiteProtoSubject(metadata, actual) { + fun hasStructureThatMatches(init: ExpressionComparator.() -> Unit) { + // TODO: maybe verify that all aspects are verified? + ExpressionComparator.createFromExpression(actual).also(init) + } + + // TODO: update DSL to not have return values (since it's unnecessary). + @ExpressionComparatorMarker + class ExpressionComparator private constructor(private val expression: MathExpression) { + // TODO: convert to constant comparator? + fun constant(init: ConstantComparator.() -> Unit): ConstantComparator = + ConstantComparator.createFromExpression(expression).also(init) + + fun variable(init: VariableComparator.() -> Unit): VariableComparator = + VariableComparator.createFromExpression(expression).also(init) + + fun addition(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.ADD + ).also(init) + } + + fun subtraction(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.SUBTRACT + ).also(init) + } + + fun multiplication(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.MULTIPLY + ).also(init) + } + + fun division(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.DIVIDE + ).also(init) + } + + fun exponentiation(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.EXPONENTIATE + ).also(init) + } + + fun negation(init: UnaryOperationComparator.() -> Unit): UnaryOperationComparator { + return UnaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathUnaryOperation.Operator.NEGATE + ).also(init) + } + + fun positive(init: UnaryOperationComparator.() -> Unit): UnaryOperationComparator { + return UnaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathUnaryOperation.Operator.POSITIVE + ).also(init) + } + + fun functionCallTo( + type: MathFunctionCall.FunctionType, + init: FunctionCallComparator.() -> Unit + ): FunctionCallComparator { + return FunctionCallComparator.createFromExpression( + expression, + expectedFunctionType = type + ).also(init) + } + + fun group(init: ExpressionComparator.() -> Unit): ExpressionComparator { + return createFromExpression(expression.group).also(init) + } + + internal companion object { + fun createFromExpression(expression: MathExpression): ExpressionComparator = + ExpressionComparator(expression) + } + } + + @ExpressionComparatorMarker + class ConstantComparator private constructor(private val constant: Real) { + fun withValueThat(): RealSubject = assertThat(constant) + + internal companion object { + fun createFromExpression(expression: MathExpression): ConstantComparator { + assertThat(expression.expressionTypeCase).isEqualTo(CONSTANT) + return ConstantComparator(expression.constant) + } + } + } + + @ExpressionComparatorMarker + class VariableComparator private constructor(private val variableName: String) { + fun withNameThat(): StringSubject = assertThat(variableName) + + internal companion object { + fun createFromExpression(expression: MathExpression): VariableComparator { + assertThat(expression.expressionTypeCase).isEqualTo(VARIABLE) + return VariableComparator(expression.variable) + } + } + } + + @ExpressionComparatorMarker + class BinaryOperationComparator private constructor( + private val operation: MathBinaryOperation + ) { + fun leftOperand(init: ExpressionComparator.() -> Unit): ExpressionComparator = + ExpressionComparator.createFromExpression(operation.leftOperand).also(init) + + fun rightOperand(init: ExpressionComparator.() -> Unit): ExpressionComparator = + ExpressionComparator.createFromExpression(operation.rightOperand).also(init) + + internal companion object { + fun createFromExpression( + expression: MathExpression, + expectedOperator: MathBinaryOperation.Operator + ): BinaryOperationComparator { + assertThat(expression.expressionTypeCase).isEqualTo(BINARY_OPERATION) + assertWithMessage("Expected binary operation with operator: $expectedOperator") + .that(expression.binaryOperation.operator) + .isEqualTo(expectedOperator) + return BinaryOperationComparator(expression.binaryOperation) + } + } + } + + @ExpressionComparatorMarker + class UnaryOperationComparator private constructor( + private val operation: MathUnaryOperation + ) { + fun operand(init: ExpressionComparator.() -> Unit): ExpressionComparator = + ExpressionComparator.createFromExpression(operation.operand).also(init) + + internal companion object { + fun createFromExpression( + expression: MathExpression, + expectedOperator: MathUnaryOperation.Operator + ): UnaryOperationComparator { + assertThat(expression.expressionTypeCase).isEqualTo(UNARY_OPERATION) + assertWithMessage("Expected unary operation with operator: $expectedOperator") + .that(expression.unaryOperation.operator) + .isEqualTo(expectedOperator) + return UnaryOperationComparator(expression.unaryOperation) + } + } + } + + @ExpressionComparatorMarker + class FunctionCallComparator private constructor( + private val functionCall: MathFunctionCall + ) { + fun argument(init: ExpressionComparator.() -> Unit): ExpressionComparator = + ExpressionComparator.createFromExpression(functionCall.argument).also(init) + + internal companion object { + fun createFromExpression( + expression: MathExpression, + expectedFunctionType: MathFunctionCall.FunctionType + ): FunctionCallComparator { + assertThat(expression.expressionTypeCase).isEqualTo(FUNCTION_CALL) + assertWithMessage("Expected function call to: $expectedFunctionType") + .that(expression.functionCall.functionType) + .isEqualTo(expectedFunctionType) + return FunctionCallComparator(expression.functionCall) + } + } + } + + companion object { + @DslMarker private annotation class ExpressionComparatorMarker + + fun assertThat(actual: MathExpression): MathExpressionSubject = + assertAbout(::MathExpressionSubject).that(actual) + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt new file mode 100644 index 00000000000..8f9edddda0b --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt @@ -0,0 +1,41 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.DoubleSubject +import com.google.common.truth.FailureMetadata +import com.google.common.truth.IntegerSubject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import com.google.common.truth.extensions.proto.LiteProtoSubject +import org.oppia.android.app.model.Real +import org.oppia.android.testing.math.FractionSubject.Companion.assertThat + +class RealSubject( + metadata: FailureMetadata, + private val actual: Real +) : LiteProtoSubject(metadata, actual) { + fun isRationalThat(): FractionSubject { + verifyTypeToBe(Real.RealTypeCase.RATIONAL) + return assertThat(actual.rational) + } + + fun isIrrationalThat(): DoubleSubject { + verifyTypeToBe(Real.RealTypeCase.IRRATIONAL) + return assertThat(actual.irrational) + } + + fun isIntegerThat(): IntegerSubject { + verifyTypeToBe(Real.RealTypeCase.INTEGER) + return assertThat(actual.integer) + } + + private fun verifyTypeToBe(expected: Real.RealTypeCase) { + assertWithMessage("Expected real type to be $expected, not: ${actual.realTypeCase}") + .that(actual.realTypeCase) + .isEqualTo(expected) + } + + companion object { + fun assertThat(actual: Real): RealSubject = assertAbout(::RealSubject).that(actual) + } +} From e45635d0adbd38c5f44521323e4b8f5329f3b8f3 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 19:20:25 -0800 Subject: [PATCH 007/134] Add protos + testing lib for commutative exprs. --- model/src/main/proto/math.proto | 53 ++++++ .../oppia/android/testing/math/BUILD.bazel | 17 ++ .../math/ComparableOperationListSubject.kt | 172 ++++++++++++++++++ 3 files changed, 242 insertions(+) create mode 100644 testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index 7dcbf780370..168db946cd6 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -96,3 +96,56 @@ message MathEquation { MathExpression left_side = 1; MathExpression right_side = 2; } + +// Represents a list of comparable mathematics operations. 'Comparable' here means that this +// structure provides a trivial way to compare commutative operations (i.e. by extracting terms from +// multiple subsequent commutative operations into lists that can be deterministically sorted). This +// structure is meant to provide a means to compare two expressions without considering +// associativity or commutativity (though the latter requires the operation lists stored within this +// structure to be sorted before using standard proto equals checking). +message ComparableOperationList { + message ComparableOperation { + // Treat this operation (e.g. x) as negated (e.g. -x). + bool is_negated = 1; + + // Treat this operation (e.g. x) as a multiplicative inverse (e.g. 1/x). + bool is_inverted = 2; + + oneof comparison_type { + CommutativeAccumulation commutative_accumulation = 3; + NonCommutativeOperation non_commutative_operation = 4; + Real constant_term = 5; + string variable_term = 6; + } + } + // Represents an accumulation of operations (such as a summation or product). This helps simplify + // comparison across commutative boundaries by collecting terms into sortable lists, such as the + // expression 1+2+3 becoming [1,2,3] and trivially comparable to [3,2,1] from 3+2+1. + // + // Subsequent subtractions are treated as additions with each term arithmetically negated (i.e. + // f(x)=-x). Similarly, divisions are considered multiplications with each divisor being + // multiplicatively inverted (i.e. the reciprocal function: f(x)=1/x). + message CommutativeAccumulation { + enum AccumulationType { + ACCUMULATION_TYPE_UNSPECIFIED = 0; + SUMMATION = 1; + PRODUCT = 2; + } + + AccumulationType accumulation_type = 1; + repeated ComparableOperation combined_operations = 2; + } + message NonCommutativeOperation { + oneof operation_type { + BinaryOperation exponentiation = 1; + ComparableOperation square_root = 2; + } + + message BinaryOperation { + ComparableOperation left_operand = 1; + ComparableOperation right_operand = 2; + } + } + + ComparableOperation root_operation = 1; +} diff --git a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel index a1453dd4496..ed7e885c3c0 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel @@ -6,6 +6,23 @@ load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") # TODO(#2747): Move these libraries to be under utility/.../math/testing. +kt_android_library( + name = "comparable_operation_list_subject", + testonly = True, + srcs = [ + "ComparableOperationListSubject.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":real_subject", + "//model/src/main/proto:math_java_proto_lite", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + ], +) + kt_android_library( name = "fraction_subject", testonly = True, diff --git a/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt new file mode 100644 index 00000000000..ce32ec28d2b --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt @@ -0,0 +1,172 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.BooleanSubject +import com.google.common.truth.FailureMetadata +import com.google.common.truth.IntegerSubject +import com.google.common.truth.StringSubject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.extensions.proto.LiteProtoSubject +import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase +import org.oppia.android.app.model.Real +import org.oppia.android.testing.math.RealSubject.Companion.assertThat + +class ComparableOperationListSubject( + metadata: FailureMetadata, + private val actual: ComparableOperationList +) : LiteProtoSubject(metadata, actual) { + fun hasStructureThatMatches(init: ComparableOperationComparator.() -> Unit) { + ComparableOperationComparator.createFrom(actual.rootOperation).also(init) + } + + @ComparableOperationComparatorMarker + class ComparableOperationComparator private constructor( + private val operation: ComparableOperation + ) { + fun hasNegatedPropertyThat(): BooleanSubject = assertThat(operation.isNegated) + + fun hasInvertedPropertyThat(): BooleanSubject = assertThat(operation.isInverted) + + fun commutativeAccumulationWithType( + type: ComparableOperationList.CommutativeAccumulation.AccumulationType, + init: CommutativeAccumulationComparator.() -> Unit + ): CommutativeAccumulationComparator = + CommutativeAccumulationComparator.createFrom(type, operation).also(init) + + fun nonCommutativeOperation( + init: NonCommutativeOperationComparator.() -> Unit + ): NonCommutativeOperationComparator = + NonCommutativeOperationComparator.createFrom(operation).also(init) + + fun constantTerm(init: ConstantTermComparator.() -> Unit): ConstantTermComparator = + ConstantTermComparator.createFrom(operation).also(init) + + fun variableTerm(init: VariableTermComparator.() -> Unit): VariableTermComparator = + VariableTermComparator.createFrom(operation).also(init) + + internal companion object { + fun createFrom(operation: ComparableOperation): ComparableOperationComparator = + ComparableOperationComparator(operation) + } + } + + @ComparableOperationComparatorMarker + class CommutativeAccumulationComparator private constructor( + private val accumulation: ComparableOperationList.CommutativeAccumulation + ) { + fun hasOperandCountThat(): IntegerSubject = assertThat(accumulation.combinedOperationsCount) + + fun index( + index: Int, + init: ComparableOperationComparator.() -> Unit + ): ComparableOperationComparator { + return ComparableOperationComparator.createFrom( + accumulation.combinedOperationsList[index] + ).also(init) + } + + internal companion object { + fun createFrom( + type: ComparableOperationList.CommutativeAccumulation.AccumulationType, + operation: ComparableOperation + ): CommutativeAccumulationComparator { + assertThat(operation.comparisonTypeCase) + .isEqualTo(ComparisonTypeCase.COMMUTATIVE_ACCUMULATION) + assertThat(operation.commutativeAccumulation.accumulationType).isEqualTo(type) + return CommutativeAccumulationComparator(operation.commutativeAccumulation) + } + } + } + + @ComparableOperationComparatorMarker + class NonCommutativeOperationComparator private constructor( + private val operation: ComparableOperationList.NonCommutativeOperation + ) { + fun exponentiation(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + verifyTypeAs( + ComparableOperationList.NonCommutativeOperation.OperationTypeCase.EXPONENTIATION + ) + return BinaryOperationComparator.createFrom(operation.exponentiation).also(init) + } + + fun squareRootWithArgument( + init: ComparableOperationComparator.() -> Unit + ): ComparableOperationComparator { + verifyTypeAs(ComparableOperationList.NonCommutativeOperation.OperationTypeCase.SQUARE_ROOT) + return ComparableOperationComparator.createFrom(operation.squareRoot).also(init) + } + + private fun verifyTypeAs( + type: ComparableOperationList.NonCommutativeOperation.OperationTypeCase + ) { + assertThat(operation.operationTypeCase).isEqualTo(type) + } + + internal companion object { + fun createFrom(operation: ComparableOperation): NonCommutativeOperationComparator { + assertThat(operation.comparisonTypeCase) + .isEqualTo(ComparisonTypeCase.NON_COMMUTATIVE_OPERATION) + return NonCommutativeOperationComparator(operation.nonCommutativeOperation) + } + } + } + + @ComparableOperationComparatorMarker + class BinaryOperationComparator private constructor( + private val operation: ComparableOperationList.NonCommutativeOperation.BinaryOperation + ) { + fun leftOperand( + init: ComparableOperationComparator.() -> Unit + ): ComparableOperationComparator = + ComparableOperationComparator.createFrom(operation.leftOperand).also(init) + + fun rightOperand( + init: ComparableOperationComparator.() -> Unit + ): ComparableOperationComparator = + ComparableOperationComparator.createFrom(operation.rightOperand).also(init) + + internal companion object { + fun createFrom( + operation: ComparableOperationList.NonCommutativeOperation.BinaryOperation + ): BinaryOperationComparator = BinaryOperationComparator(operation) + } + } + + @ComparableOperationComparatorMarker + class ConstantTermComparator private constructor( + private val constant: Real + ) { + fun withValueThat(): RealSubject = assertThat(constant) + + internal companion object { + fun createFrom(operation: ComparableOperation): ConstantTermComparator { + assertThat(operation.comparisonTypeCase).isEqualTo(ComparisonTypeCase.CONSTANT_TERM) + return ConstantTermComparator(operation.constantTerm) + } + } + } + + @ComparableOperationComparatorMarker + class VariableTermComparator private constructor( + private val variableName: String + ) { + fun withNameThat(): StringSubject = assertThat(variableName) + + internal companion object { + fun createFrom(operation: ComparableOperation): VariableTermComparator { + assertThat(operation.comparisonTypeCase).isEqualTo(ComparisonTypeCase.VARIABLE_TERM) + return VariableTermComparator(operation.variableTerm) + } + } + } + + companion object { + // See: https://kotlinlang.org/docs/type-safe-builders.html. + @DslMarker private annotation class ComparableOperationComparatorMarker + + fun assertThat(actual: ComparableOperationList): ComparableOperationListSubject = + assertAbout(::ComparableOperationListSubject).that(actual) + } +} From da5d72d1cc5bf3e7a1493be452a40ec3d4bc06b9 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 19:50:42 -0800 Subject: [PATCH 008/134] Add protos & test libs for polynomials. --- .../util/InteractionObjectExtensions.kt | 9 --- model/src/main/proto/math.proto | 14 ++++ .../oppia/android/testing/math/BUILD.bazel | 18 +++++ .../android/testing/math/PolynomialSubject.kt | 79 +++++++++++++++++++ .../org/oppia/android/util/math/BUILD.bazel | 2 + .../android/util/math/FloatExtensions.kt | 2 + .../android/util/math/FractionExtensions.kt | 59 ++++++++++++++ .../android/util/math/PolynomialExtensions.kt | 56 +++++++++++++ .../oppia/android/util/math/RealExtensions.kt | 53 +++++++++++++ 9 files changed, 283 insertions(+), 9 deletions(-) create mode 100644 testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt 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 2e37e34e0f7..11d3af018a9 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 @@ -107,15 +107,6 @@ private fun ImageWithRegions.toAnswerString(): String = private fun ClickOnImage.toAnswerString(): String = "[(${clickedRegionsList.joinToString()}), (${clickPosition.x}, ${clickPosition.y})]" -// https://github.com/oppia/oppia/blob/37285a/core/templates/dev/head/domain/objects/FractionObjectFactory.ts#L47 -private fun Fraction.toAnswerString(): String { - val fractionString = if (numerator != 0) "$numerator/$denominator" else "" - val mixedString = if (wholeNumber != 0) "$wholeNumber $fractionString" else "" - val positiveFractionString = if (mixedString.isNotEmpty()) mixedString else fractionString - val negativeString = if (isNegative) "-" else "" - return if (positiveFractionString.isNotEmpty()) "$negativeString$positiveFractionString" else "0" -} - private fun TranslatableHtmlContentId.toAnswerString(): String { return "content_id=$contentId" } diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index 168db946cd6..95127692db6 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -149,3 +149,17 @@ message ComparableOperationList { ComparableOperation root_operation = 1; } + +message Polynomial { + repeated Term term = 1; + + message Term { + Real coefficient = 1; + repeated Variable variable = 2; + + message Variable { + string name = 1; + uint32 power = 2; + } + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel index ed7e885c3c0..275084d309b 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel @@ -74,6 +74,24 @@ kt_android_library( ], ) +kt_android_library( + name = "polynomial_subject", + testonly = True, + srcs = [ + "PolynomialSubject.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":real_subject", + "//model/src/main/proto:math_java_proto_lite", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + ], +) + kt_android_library( name = "real_subject", testonly = True, diff --git a/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt new file mode 100644 index 00000000000..6b05db139a5 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt @@ -0,0 +1,79 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.FailureMetadata +import com.google.common.truth.IntegerSubject +import com.google.common.truth.StringSubject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import com.google.common.truth.extensions.proto.LiteProtoSubject +import org.oppia.android.app.model.Polynomial +import org.oppia.android.testing.math.RealSubject.Companion.assertThat +import org.oppia.android.util.math.getConstant +import org.oppia.android.util.math.isConstant +import org.oppia.android.util.math.toPlainText + +class PolynomialSubject( + metadata: FailureMetadata, + private val actual: Polynomial? +) : LiteProtoSubject(metadata, actual) { + private val nonNullActual by lazy { + checkNotNull(actual) { + "Expected polynomial to be defined, not null (is the expression/equation not a valid" + + " polynomial?)" + } + } + + fun isNotValidPolynomial() { + // TODO: use toPlainText here. + assertWithMessage( + "Expected polynomial to be undefined, but was: ${actual?.toPlainText()}" + ).that(actual).isNull() + } + + fun isConstantThat(): RealSubject { + // TODO: use toPlainText here. + assertWithMessage("Expected polynomial to be constant, but was: $nonNullActual") + .that(nonNullActual.isConstant()) + .isTrue() + return assertThat(nonNullActual.getConstant()) + } + + fun hasTermCountThat(): IntegerSubject = assertThat(nonNullActual.termCount) + + fun term(index: Int): PolynomialTermSubject = assertThat(nonNullActual.termList[index]) + + fun evaluatesToPlainTextThat(): StringSubject = assertThat(nonNullActual.toPlainText()) + + companion object { + fun assertThat(actual: Polynomial?): PolynomialSubject = + assertAbout(::PolynomialSubject).that(actual) + + private fun assertThat(actual: Polynomial.Term): PolynomialTermSubject = + assertAbout(::PolynomialTermSubject).that(actual) + + private fun assertThat(actual: Polynomial.Term.Variable): PolynomialTermVariableSubject = + assertAbout(::PolynomialTermVariableSubject).that(actual) + } + + class PolynomialTermSubject( + metadata: FailureMetadata, + private val actual: Polynomial.Term + ) : LiteProtoSubject(metadata, actual) { + fun hasCoefficientThat(): RealSubject = assertThat(actual.coefficient) + + fun hasVariableCountThat(): IntegerSubject = assertThat(actual.variableCount) + + fun variable(index: Int): PolynomialTermVariableSubject = + assertThat(actual.variableList[index]) + } + + class PolynomialTermVariableSubject( + metadata: FailureMetadata, + private val actual: Polynomial.Term.Variable + ) : LiteProtoSubject(metadata, actual) { + fun hasNameThat(): StringSubject = assertThat(actual.name) + + fun hasPowerThat(): IntegerSubject = assertThat(actual.power) + } +} 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 index 4b84961d297..054b7df78bb 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -9,7 +9,9 @@ kt_android_library( srcs = [ "FloatExtensions.kt", "FractionExtensions.kt", + "PolynomialExtensions.kt", "RatioExtensions.kt", + "RealExtensions.kt", ], visibility = [ "//:oppia_api_visibility", diff --git a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt index 62504046c78..2ca5ece9da3 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt @@ -14,3 +14,5 @@ fun Float.approximatelyEquals(other: Float): Boolean { fun Double.approximatelyEquals(other: Double): Boolean { return abs(this - other) < FLOAT_EQUALITY_INTERVAL } + +fun Double.toPlainString(): String = toBigDecimal().toPlainString() diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index d4a57faf9be..1da9fef1857 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -2,6 +2,19 @@ package org.oppia.android.util.math import org.oppia.android.app.model.Fraction +/** Returns whether this fraction has a fractional component. */ +fun Fraction.hasFractionalPart(): Boolean { + return numerator != 0 +} + +/** + * Returns whether this fraction only represents a whole number. Note that for the fraction '0' this + * will return true. + */ +fun Fraction.isOnlyWholeNumber(): Boolean { + return !hasFractionalPart() +} + /** * Returns a [Double] version of this fraction. * @@ -13,6 +26,35 @@ fun Fraction.toDouble(): Double { return if (isNegative) -doubleVal else doubleVal } +/** + * Returns a submittable answer string representation of this fraction (note that this may not be + * the verbatim string originally submitted by the user, if any. + */ +fun Fraction.toAnswerString(): String { + return when { + isOnlyWholeNumber() -> { + // Fraction is only a whole number. + if (isNegative) "-$wholeNumber" else "$wholeNumber" + } + wholeNumber == 0 -> { + // Fraction contains just a fraction (no whole number). + when (denominator) { + 1 -> if (isNegative) "-$numerator" else "$numerator" + else -> if (isNegative) "-$numerator/$denominator" else "$numerator/$denominator" + } + } + else -> { + // Otherwise it's a mixed number. Note that the denominator is always shown here to account + // for strange cases that would require evaluation to resolve, such as: "2 2/1". + if (isNegative) { + "-$wholeNumber $numerator/$denominator" + } else { + "$wholeNumber $numerator/$denominator" + } + } + } +} + /** * Returns this fraction in its most simplified form. * @@ -26,6 +68,23 @@ fun Fraction.toSimplestForm(): Fraction { }.build() } +/** + * Returns this fraction in an improper form (that is, with a 0 whole number and only fractional + * parts). + */ +fun Fraction.toImproperForm(): Fraction { + val newNumerator = numerator + (denominator * wholeNumber) + return toBuilder().apply { + numerator = newNumerator + wholeNumber = 0 + }.build() +} + +/** Returns the negated form of this fraction. */ +operator fun Fraction.unaryMinus(): Fraction { + return toBuilder().apply { isNegative = !this@unaryMinus.isNegative }.build() +} + /** Returns the greatest common divisor between two integers. */ fun gcd(x: Int, y: Int): Int { return if (y == 0) x else gcd(y, x % y) diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt new file mode 100644 index 00000000000..e1b98934566 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -0,0 +1,56 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.Polynomial +import org.oppia.android.app.model.Polynomial.Term +import org.oppia.android.app.model.Polynomial.Term.Variable +import org.oppia.android.app.model.Real +import org.oppia.android.app.model.Real.RealTypeCase.INTEGER +import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL +import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL +import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET + +/** Returns whether this polynomial is a constant-only polynomial (contains no variables). */ +fun Polynomial.isConstant(): Boolean = termCount == 1 && getTerm(0).variableCount == 0 + +/** + * Returns the first term coefficient from this polynomial. This corresponds to the whole value of + * the polynomial iff isConstant() returns true, otherwise this value isn't useful. + * + * Note that this function can throw if the polynomial is empty (so isConstant() should always be + * checked first). + */ +fun Polynomial.getConstant(): Real = getTerm(0).coefficient + +fun Polynomial.toPlainText(): String { + return termList.map { it.toPlainText() }.reduce { acc, termAnswerStr -> + if (termAnswerStr.startsWith("-")) { + "$acc - ${termAnswerStr.drop(1)}" + } else "$acc + $termAnswerStr" + } +} + +private fun Term.toPlainText(): String { + val productValues = mutableListOf() + + // Include the coefficient if there is one (coefficients of 1 are ignored only if there are + // variables present). + productValues += when { + variableList.isEmpty() || !abs(coefficient).isApproximatelyEqualTo(1.0) -> when { + coefficient.isRational() && variableList.isNotEmpty() -> "(${coefficient.toPlainText()})" + else -> coefficient.toPlainText() + } + coefficient.isNegative() -> "-" + else -> "" + } + + // Include any present variables. + productValues += variableList.map(Variable::toPlainText) + + // Take the product of all relevant values of the term. + return productValues.joinToString(separator = "") +} + +private fun Variable.toPlainText(): String { + return if (power > 1) "$name^$power" else name +} + diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt new file mode 100644 index 00000000000..6df36abd3b6 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -0,0 +1,53 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.Real +import org.oppia.android.app.model.Real.RealTypeCase.INTEGER +import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL +import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL +import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET + +fun Real.isRational(): Boolean = realTypeCase == RATIONAL + +fun Real.isNegative(): Boolean = when (realTypeCase) { + RATIONAL -> rational.isNegative + IRRATIONAL -> irrational < 0 + INTEGER -> integer < 0 + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") +} + +fun Real.toDouble(): Double { + return when (realTypeCase) { + RATIONAL -> rational.toDouble() + INTEGER -> integer.toDouble() + IRRATIONAL -> irrational + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + } +} + +fun Real.toPlainText(): String = when (realTypeCase) { + // Note that the rational part is first converted to an improper fraction since mixed fractions + // can't be expressed as a single coefficient in typical polynomial syntax). + RATIONAL -> rational.toImproperForm().toAnswerString() + IRRATIONAL -> irrational.toPlainString() + INTEGER -> integer.toString() + REALTYPE_NOT_SET, null -> "" +} + +fun Real.isApproximatelyEqualTo(value: Double): Boolean { + return toDouble().approximatelyEquals(value) +} + +operator fun Real.unaryMinus(): Real { + return when (realTypeCase) { + RATIONAL -> recompute { it.setRational(-rational) } + IRRATIONAL -> recompute { it.setIrrational(-irrational) } + INTEGER -> recompute { it.setInteger(-integer) } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + } +} + +fun abs(real: Real): Real = if (real.isNegative()) -real else real + +private fun Real.recompute(transform: (Real.Builder) -> Real.Builder): Real { + return transform(newBuilderForType()).build() +} From 7c36fdfbe93e4f13319344a4dc118825c244304a Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 23:15:30 -0800 Subject: [PATCH 009/134] Lint fix. --- ...utIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 af698227520..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 @@ -36,7 +36,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider input: Fraction, writtenTranslationContext: WrittenTranslationContext ): Boolean { - return answer.toDouble().approximatelyEquals(input.toDouble()) - && answer == input.toSimplestForm() + return answer.toDouble().approximatelyEquals(input.toDouble()) && + answer == input.toSimplestForm() } } From d430f8c826054b6d9c4a7d1fc5b7da9f4297cc1f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 23:17:24 -0800 Subject: [PATCH 010/134] Lint fixes. --- .../oppia/android/domain/util/InteractionObjectExtensions.kt | 1 - .../java/org/oppia/android/testing/math/PolynomialSubject.kt | 2 +- .../java/org/oppia/android/util/math/PolynomialExtensions.kt | 5 ----- 3 files changed, 1 insertion(+), 7 deletions(-) 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 11d3af018a9..32f9123e852 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 @@ -1,7 +1,6 @@ package org.oppia.android.domain.util import org.oppia.android.app.model.ClickOnImage -import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.ImageWithRegions import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.BOOL_VALUE diff --git a/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt index 6b05db139a5..bb4a3d970d5 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt @@ -44,7 +44,7 @@ class PolynomialSubject( fun term(index: Int): PolynomialTermSubject = assertThat(nonNullActual.termList[index]) fun evaluatesToPlainTextThat(): StringSubject = assertThat(nonNullActual.toPlainText()) - + companion object { fun assertThat(actual: Polynomial?): PolynomialSubject = assertAbout(::PolynomialSubject).that(actual) diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index e1b98934566..a4ba72213be 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -4,10 +4,6 @@ import org.oppia.android.app.model.Polynomial import org.oppia.android.app.model.Polynomial.Term import org.oppia.android.app.model.Polynomial.Term.Variable import org.oppia.android.app.model.Real -import org.oppia.android.app.model.Real.RealTypeCase.INTEGER -import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL -import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL -import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET /** Returns whether this polynomial is a constant-only polynomial (contains no variables). */ fun Polynomial.isConstant(): Boolean = termCount == 1 && getTerm(0).variableCount == 0 @@ -53,4 +49,3 @@ private fun Term.toPlainText(): String { private fun Variable.toPlainText(): String { return if (power > 1) "$name^$power" else name } - From 0099d67a87ea22f7ad26fb3536453646127dc077 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 23:34:12 -0800 Subject: [PATCH 011/134] Add math tokenizer + utility & tests. This is mostly copied from #2173. --- .../org/oppia/android/util/math/BUILD.bazel | 21 + .../oppia/android/util/math/MathTokenizer.kt | 381 ++++++++++++++++++ .../android/util/math/PeekableIterator.kt | 38 ++ .../org/oppia/android/util/math/BUILD.bazel | 18 + .../android/util/math/MathTokenizerTest.kt | 195 +++++++++ 5 files changed, 653 insertions(+) create mode 100644 utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt 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 index 054b7df78bb..1c099b59d0f 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -20,3 +20,24 @@ kt_android_library( "//model/src/main/proto:math_java_proto_lite", ], ) + +kt_android_library( + name = "tokenizer", + srcs = [ + "MathTokenizer.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":peekable_iterator", + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "peekable_iterator", + srcs = [ + "PeekableIterator.kt", + ], +) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt new file mode 100644 index 00000000000..37ca0410cd0 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt @@ -0,0 +1,381 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import java.lang.StringBuilder + +// TODO: rename to MathTokenizer & add documentation. +// TODO: consider a more efficient implementation that uses 1 underlying buffer (which could still +// be sequence-backed) with a forced lookahead-of-1 API, to also avoid rebuffering when parsing +// sequences of characters like for integers. + +// TODO: add customization to omit certain symbols (such as variables for numeric expressions?) +class MathTokenizer private constructor() { + companion object { + fun tokenize(input: String): Sequence = tokenize(input.toCharArray().asSequence()) + + fun tokenize(input: Sequence): Sequence { + val chars = PeekableIterator.fromSequence(input) + return generateSequence { + // Consume any whitespace that might precede a valid token. + chars.consumeWhitespace() + + // Parse the next token from the underlying sequence. + when (chars.peek()) { + in '0'..'9' -> tokenizeIntegerOrRealNumber(chars) + in 'a'..'z', in 'A'..'Z' -> tokenizeVariableOrFunctionName(chars) + '√' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.SquareRootSymbol(startIndex, endIndex) + } + '+' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.PlusSymbol(startIndex, endIndex) + } + // TODO: add tests for different subtraction/minus symbols. + '-', '−' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.MinusSymbol(startIndex, endIndex) + } + '*', '×' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.MultiplySymbol(startIndex, endIndex) + } + '/', '÷' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.DivideSymbol(startIndex, endIndex) + } + '^' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.ExponentiationSymbol(startIndex, endIndex) + } + '=' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.EqualsSymbol(startIndex, endIndex) + } + '(' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.LeftParenthesisSymbol(startIndex, endIndex) + } + ')' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.RightParenthesisSymbol(startIndex, endIndex) + } + null -> null // End of stream. + // Invalid character. + else -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.InvalidToken(startIndex, endIndex) + } + } + } + } + + private fun tokenizeIntegerOrRealNumber(chars: PeekableIterator): Token { + val startIndex = chars.getRetrievalCount() + val integerPart1 = + parseInteger(chars) + ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) + chars.consumeWhitespace() // Whitespace is allowed between digits and the '.'. + return if (chars.peek() == '.') { + chars.next() // Parse the "." since it will be re-added later. + chars.consumeWhitespace() // Whitespace is allowed between the '.' and following digits. + + // Another integer must follow the ".". + val integerPart2 = parseInteger(chars) + ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) + + // TODO: validate that the result isn't NaN or INF. + val doubleValue = "$integerPart1.$integerPart2".toDoubleOrNull() + ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) + Token.PositiveRealNumber(doubleValue, startIndex, endIndex = chars.getRetrievalCount()) + } else { + Token.PositiveInteger( + integerPart1.toIntOrNull() + ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()), + startIndex, + endIndex = chars.getRetrievalCount() + ) + } + } + + private fun tokenizeVariableOrFunctionName(chars: PeekableIterator): Token { + val startIndex = chars.getRetrievalCount() + val firstChar = chars.next() + + // latin_letter = lowercase_latin_letter | uppercase_latin_letter ; + // variable = latin_letter ; + return tokenizeFunctionName(firstChar, startIndex, chars) + ?: Token.VariableName( + firstChar.toString(), startIndex, endIndex = chars.getRetrievalCount() + ) + } + + private fun tokenizeFunctionName( + currChar: Char, + startIndex: Int, + chars: PeekableIterator + ): Token? { + // allowed_function_name = "sqrt" ; + // disallowed_function_name = + // "exp" | "log" | "log10" | "ln" | "sin" | "cos" | "tan" | "cot" | "csc" + // | "sec" | "atan" | "asin" | "acos" | "abs" ; + // function_name = allowed_function_name | disallowed_function_name ; + val nextChar = chars.peek() + return when (currChar) { + 'a' -> { + // abs, acos, asin, atan, or variable. + when (nextChar) { + 'b' -> + tokenizeExpectedFunction(name = "abs", isAllowedFunction = false, startIndex, chars) + 'c' -> + tokenizeExpectedFunction(name = "acos", isAllowedFunction = false, startIndex, chars) + 's' -> + tokenizeExpectedFunction(name = "asin", isAllowedFunction = false, startIndex, chars) + 't' -> + tokenizeExpectedFunction(name = "atan", isAllowedFunction = false, startIndex, chars) + else -> null // Must be a variable. + } + } + 'c' -> { + // cos, cot, csc, or variable. + when (nextChar) { + 'o' -> { + chars.next() // Skip the 'o' to go to the last character. + val name = if (chars.peek() == 's') { + chars.expectNextMatches { it == 's' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + "cos" + } else { + // Otherwise, it must be 'c' for 'cot' since the parser can't backtrack. + chars.expectNextMatches { it == 't' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + "cot" + } + Token.FunctionName( + name, isAllowedFunction = false, startIndex, endIndex = chars.getRetrievalCount() + ) + } + 's' -> + tokenizeExpectedFunction(name = "csc", isAllowedFunction = false, startIndex, chars) + else -> null // Must be a variable. + } + } + 'e' -> { + // exp or variable. + if (nextChar == 'x') { + tokenizeExpectedFunction(name = "exp", isAllowedFunction = false, startIndex, chars) + } else null // Must be a variable. + } + 'l' -> { + // ln, log, log10, or variable. + when (nextChar) { + 'n' -> + tokenizeExpectedFunction(name = "ln", isAllowedFunction = false, startIndex, chars) + 'o' -> { + // Skip the 'o'. Following the 'o' must be a 'g' since the parser can't backtrack. + chars.next() + chars.expectNextMatches { it == 'g' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + val name = if (chars.peek() == '1') { + // '10' must be next for 'log10'. + chars.expectNextMatches { it == '1' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + chars.expectNextMatches { it == '0' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + "log10" + } else "log" + Token.FunctionName( + name, isAllowedFunction = false, startIndex, endIndex = chars.getRetrievalCount() + ) + } + else -> null // Must be a variable. + } + } + 's' -> { + // sec, sin, sqrt, or variable. + when (nextChar) { + 'e' -> + tokenizeExpectedFunction(name = "sec", isAllowedFunction = false, startIndex, chars) + 'i' -> + tokenizeExpectedFunction(name = "sin", isAllowedFunction = false, startIndex, chars) + 'q' -> + tokenizeExpectedFunction(name = "sqrt", isAllowedFunction = true, startIndex, chars) + else -> null // Must be a variable. + } + } + 't' -> { + // tan or variable. + if (nextChar == 'a') { + tokenizeExpectedFunction(name = "tan", isAllowedFunction = false, startIndex, chars) + } else null // Must be a variable. + } + else -> null // Must be a variable since no known functions match the first character. + } + } + + private fun tokenizeExpectedFunction( + name: String, + isAllowedFunction: Boolean, + startIndex: Int, + chars: PeekableIterator + ): Token { + return chars.expectNextCharsForFunctionName(name.substring(1), startIndex) + ?: Token.FunctionName( + name, isAllowedFunction, startIndex, endIndex = chars.getRetrievalCount() + ) + } + + private fun tokenizeSymbol(chars: PeekableIterator, factory: (Int, Int) -> Token): Token { + val startIndex = chars.getRetrievalCount() + chars.next() // Parse the symbol. + val endIndex = chars.getRetrievalCount() + return factory(startIndex, endIndex) + } + + private fun parseInteger(chars: PeekableIterator): String? { + val integerBuilder = StringBuilder() + while (chars.peek() in '0'..'9') { + integerBuilder.append(chars.next()) + } + return if (integerBuilder.isNotEmpty()) { + integerBuilder.toString() + } else null // Failed to parse; no digits. + } + + interface UnaryOperatorToken { + fun getUnaryOperator(): MathUnaryOperation.Operator + } + + interface BinaryOperatorToken { + fun getBinaryOperator(): MathBinaryOperation.Operator + } + + sealed class Token { + /** The index in the input stream at which point this token begins. */ + abstract val startIndex: Int + + /** The (exclusive) index in the input stream at which point this token ends. */ + abstract val endIndex: Int + + class PositiveInteger( + val parsedValue: Int, + override val startIndex: Int, + override val endIndex: Int + ) : Token() + + class PositiveRealNumber( + val parsedValue: Double, + override val startIndex: Int, + override val endIndex: Int + ) : Token() + + class VariableName( + val parsedName: String, + override val startIndex: Int, + override val endIndex: Int + ) : Token() + + class FunctionName( + val parsedName: String, + val isAllowedFunction: Boolean, + override val startIndex: Int, + override val endIndex: Int + ) : Token() + + class MinusSymbol( + override val startIndex: Int, + override val endIndex: Int + ) : Token(), UnaryOperatorToken, BinaryOperatorToken { + override fun getUnaryOperator(): MathUnaryOperation.Operator = NEGATE + + override fun getBinaryOperator(): MathBinaryOperation.Operator = SUBTRACT + } + + class SquareRootSymbol(override val startIndex: Int, override val endIndex: Int) : Token() + + class PlusSymbol( + override val startIndex: Int, + override val endIndex: Int + ) : Token(), UnaryOperatorToken, BinaryOperatorToken { + override fun getUnaryOperator(): MathUnaryOperation.Operator = POSITIVE + + override fun getBinaryOperator(): MathBinaryOperation.Operator = ADD + } + + class MultiplySymbol( + override val startIndex: Int, + override val endIndex: Int + ) : Token(), BinaryOperatorToken { + override fun getBinaryOperator(): MathBinaryOperation.Operator = MULTIPLY + } + + class DivideSymbol( + override val startIndex: Int, + override val endIndex: Int + ) : Token(), BinaryOperatorToken { + override fun getBinaryOperator(): MathBinaryOperation.Operator = DIVIDE + } + + class ExponentiationSymbol( + override val startIndex: Int, + override val endIndex: Int + ) : Token(), BinaryOperatorToken { + override fun getBinaryOperator(): MathBinaryOperation.Operator = EXPONENTIATE + } + + class EqualsSymbol(override val startIndex: Int, override val endIndex: Int) : Token() + + class LeftParenthesisSymbol( + override val startIndex: Int, + override val endIndex: Int + ) : Token() + + class RightParenthesisSymbol( + override val startIndex: Int, + override val endIndex: Int + ) : Token() + + class IncompleteFunctionName( + override val startIndex: Int, + override val endIndex: Int + ) : Token() + + class InvalidToken(override val startIndex: Int, override val endIndex: Int) : Token() + } + + // TODO: consider whether to use the more correct & expensive Java Char.isWhitespace(). + private fun Char.isWhitespace(): Boolean = when (this) { + ' ', '\t', '\n', '\r' -> true + else -> false + } + + private fun PeekableIterator.consumeWhitespace() { + while (peek()?.isWhitespace() == true) next() + } + + /** + * Expects each of the characters to be next in the token stream, in the order of the string. + * All characters must be present in [this] iterator. Returns non-null if a failure occurs, + * otherwise null if all characters were confirmed to be present. If null is returned, [this] + * iterator will be at the token that comes after the last confirmed character in the string. + */ + private fun PeekableIterator.expectNextCharsForFunctionName( + chars: String, + startIndex: Int + ): Token? { + for (c in chars) { + expectNextValue { c } + ?: return Token.IncompleteFunctionName(startIndex, endIndex = getRetrievalCount()) + } + return null + } + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt b/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt new file mode 100644 index 00000000000..1a7abacc061 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt @@ -0,0 +1,38 @@ +package org.oppia.android.util.math + +class PeekableIterator(private val backingIterator: Iterator) : Iterator { + private var next: T? = null + private var count: Int = 0 + + override fun hasNext(): Boolean = next != null || backingIterator.hasNext() + + override fun next(): T = next?.also { + next = null + count++ + } ?: retrieveNext() + + fun peek(): T? { + return when { + next != null -> next + hasNext() -> retrieveNext().also { next = it } + else -> null + } + } + + fun expectNextValue(expected: () -> T): T? = expectNextMatches { it == expected() } + + fun expectNextMatches(predicate: (T) -> Boolean): T? { + // Only call the predicate if not at the end of the stream, and only call next() if the next + // value matches. + return peek()?.takeIf(predicate)?.also { next() } + } + + fun getRetrievalCount(): Int = count + + private fun retrieveNext(): T = backingIterator.next() + + companion object { + fun fromSequence(sequence: Sequence): PeekableIterator = + PeekableIterator(sequence.iterator()) + } +} 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 index 493d89d66a0..d918580ffb9 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -4,6 +4,24 @@ TODO: document load("//:oppia_android_test.bzl", "oppia_android_test") +oppia_android_test( + name = "MathTokenizerTest", + srcs = ["MathTokenizerTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.MathTokenizerTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing:assertion_helpers", + "//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:tokenizer", + ], +) + oppia_android_test( name = "RatioExtensionsTest", srcs = ["RatioExtensionsTest.kt"], diff --git a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt new file mode 100644 index 00000000000..ac7e6556b9c --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt @@ -0,0 +1,195 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.DoubleSubject +import com.google.common.truth.FailureMetadata +import com.google.common.truth.IntegerSubject +import com.google.common.truth.StringSubject +import com.google.common.truth.Subject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.util.math.MathTokenizer.Companion.Token +import org.robolectric.annotation.LooperMode + +/** Tests for [MathTokenizer]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class MathTokenizerTest { + @Test + fun testLotsOfCases() { + // TODO: split this up + // testTokenize_emptyString_producesNoTokens + val tokens1 = MathTokenizer.tokenize(" ").toList() + assertThat(tokens1).isEmpty() + + val tokens2 = MathTokenizer.tokenize(" 2 ").toList() + assertThat(tokens2).hasSize(1) + assertThat(tokens2.first()).isPositiveIntegerWhoseValue().isEqualTo(2) + + val tokens3 = MathTokenizer.tokenize(" 2.5 ").toList() + assertThat(tokens3).hasSize(1) + assertThat(tokens3.first()).isPositiveRealNumberWhoseValue().isWithin(1e-5).of(2.5) + + val tokens4 = MathTokenizer.tokenize(" x ").toList() + assertThat(tokens4).hasSize(1) + assertThat(tokens4.first()).isVariableWhoseName().isEqualTo("x") + + val tokens5 = MathTokenizer.tokenize(" z x ").toList() + assertThat(tokens5).hasSize(2) + assertThat(tokens5[0]).isVariableWhoseName().isEqualTo("z") + assertThat(tokens5[1]).isVariableWhoseName().isEqualTo("x") + + val tokens6 = MathTokenizer.tokenize("2^3^2").toList() + assertThat(tokens6).hasSize(5) + assertThat(tokens6[0]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens6[1]).isExponentiationSymbol() + assertThat(tokens6[2]).isPositiveIntegerWhoseValue().isEqualTo(3) + assertThat(tokens6[3]).isExponentiationSymbol() + assertThat(tokens6[4]).isPositiveIntegerWhoseValue().isEqualTo(2) + + val tokens7 = MathTokenizer.tokenize("sqrt(2)").toList() + assertThat(tokens7).hasSize(4) + assertThat(tokens7[0]).isFunctionWhoseName().isEqualTo("sqrt") + assertThat(tokens7[1]).isLeftParenthesisSymbol() + assertThat(tokens7[2]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens7[3]).isRightParenthesisSymbol() + + val tokens8 = MathTokenizer.tokenize("sqr(2)").toList() + assertThat(tokens8).hasSize(4) + assertThat(tokens8[0]).isIncompleteFunctionName() + assertThat(tokens8[1]).isLeftParenthesisSymbol() + assertThat(tokens8[2]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens8[3]).isRightParenthesisSymbol() + + val tokens9 = MathTokenizer.tokenize("xyz(2)").toList() + assertThat(tokens9).hasSize(6) + assertThat(tokens9[0]).isVariableWhoseName().isEqualTo("x") + assertThat(tokens9[1]).isVariableWhoseName().isEqualTo("y") + assertThat(tokens9[2]).isVariableWhoseName().isEqualTo("z") + assertThat(tokens9[3]).isLeftParenthesisSymbol() + assertThat(tokens9[4]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens9[5]).isRightParenthesisSymbol() + + val tokens10 = MathTokenizer.tokenize("732").toList() + assertThat(tokens10).hasSize(1) + assertThat(tokens10.first()).isPositiveIntegerWhoseValue().isEqualTo(732) + + val tokens11 = MathTokenizer.tokenize("73 2").toList() + assertThat(tokens11).hasSize(2) + assertThat(tokens11[0]).isPositiveIntegerWhoseValue().isEqualTo(73) + assertThat(tokens11[1]).isPositiveIntegerWhoseValue().isEqualTo(2) + + val tokens12 = MathTokenizer.tokenize("1*2-3+4^7-8/3*2+7").toList() + assertThat(tokens12).hasSize(17) + assertThat(tokens12[0]).isPositiveIntegerWhoseValue().isEqualTo(1) + assertThat(tokens12[1]).isMultiplySymbol() + assertThat(tokens12[2]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens12[3]).isMinusSymbol() + assertThat(tokens12[4]).isPositiveIntegerWhoseValue().isEqualTo(3) + assertThat(tokens12[5]).isPlusSymbol() + assertThat(tokens12[6]).isPositiveIntegerWhoseValue().isEqualTo(4) + assertThat(tokens12[7]).isExponentiationSymbol() + assertThat(tokens12[8]).isPositiveIntegerWhoseValue().isEqualTo(7) + assertThat(tokens12[9]).isMinusSymbol() + assertThat(tokens12[10]).isPositiveIntegerWhoseValue().isEqualTo(8) + assertThat(tokens12[11]).isDivideSymbol() + assertThat(tokens12[12]).isPositiveIntegerWhoseValue().isEqualTo(3) + assertThat(tokens12[13]).isMultiplySymbol() + assertThat(tokens12[14]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens12[15]).isPlusSymbol() + assertThat(tokens12[16]).isPositiveIntegerWhoseValue().isEqualTo(7) + + val tokens13 = MathTokenizer.tokenize("x = √2 × 7 ÷ 4").toList() + assertThat(tokens13).hasSize(8) + assertThat(tokens13[0]).isVariableWhoseName().isEqualTo("x") + assertThat(tokens13[1]).isEqualsSymbol() + assertThat(tokens13[2]).isSquareRootSymbol() + assertThat(tokens13[3]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens13[4]).isMultiplySymbol() + assertThat(tokens13[5]).isPositiveIntegerWhoseValue().isEqualTo(7) + assertThat(tokens13[6]).isDivideSymbol() + assertThat(tokens13[7]).isPositiveIntegerWhoseValue().isEqualTo(4) + } + + private class TokenSubject( + metadata: FailureMetadata, + private val actual: T + ) : Subject(metadata, actual) { + fun isPositiveIntegerWhoseValue(): IntegerSubject { + return assertThat(actual.asVerifiedType().parsedValue) + } + + fun isPositiveRealNumberWhoseValue(): DoubleSubject { + return assertThat(actual.asVerifiedType().parsedValue) + } + + fun isVariableWhoseName(): StringSubject { + return assertThat(actual.asVerifiedType().parsedName) + } + + fun isFunctionWhoseName(): StringSubject { + return assertThat(actual.asVerifiedType().parsedName) + } + + fun isMinusSymbol() { + actual.asVerifiedType() + } + + fun isSquareRootSymbol() { + actual.asVerifiedType() + } + + fun isPlusSymbol() { + actual.asVerifiedType() + } + + fun isMultiplySymbol() { + actual.asVerifiedType() + } + + fun isDivideSymbol() { + actual.asVerifiedType() + } + + fun isExponentiationSymbol() { + actual.asVerifiedType() + } + + fun isEqualsSymbol() { + actual.asVerifiedType() + } + + fun isLeftParenthesisSymbol() { + actual.asVerifiedType() + } + + fun isRightParenthesisSymbol() { + actual.asVerifiedType() + } + + fun isInvalidToken() { + actual.asVerifiedType() + } + + fun isIncompleteFunctionName() { + actual.asVerifiedType() + } + + private companion object { + private inline fun Token.asVerifiedType(): T { + assertThat(this).isInstanceOf(T::class.java) + return this as T + } + } + } + + private companion object { + private fun assertThat(actual: T): TokenSubject = + assertAbout(createTokenSubjectFactory()).that(actual) + + private fun createTokenSubjectFactory() = + Subject.Factory, T>(::TokenSubject) + } +} From 1d721d563071b57a4cf801dfec97f77da7663220 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 00:25:11 -0800 Subject: [PATCH 012/134] Add math expression/equation parsing support. This includes full error detection, and specific test suites for each parsing case. Much revisement is needed in the tests, and some additional issues may yet need to be fixed in the parser and/or error-detection logic. This is copied from #2173 with revisement & reduction since it's part of a multi-PR split. --- .../org/oppia/android/util/math/BUILD.bazel | 30 + .../android/util/math/MathExpressionParser.kt | 1044 +++++++++ .../android/util/math/MathParsingError.kt | 69 + .../util/math/AlgebraicEquationParserTest.kt | 227 ++ .../math/AlgebraicExpressionParserTest.kt | 1939 +++++++++++++++++ .../org/oppia/android/util/math/BUILD.bazel | 82 + .../util/math/MathExpressionParserTest.kt | 344 +++ .../util/math/NumericExpressionParserTest.kt | 1777 +++++++++++++++ 8 files changed, 5512 insertions(+) create mode 100644 utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt 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 index 1c099b59d0f..1e0da7381b5 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -21,6 +21,36 @@ kt_android_library( ], ) +kt_android_library( + name = "parsing_error", + srcs = [ + "MathParsingError.kt", + ], + visibility = [ + "//:oppia_api_visibility", + ], + deps = [ + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "parser", + srcs = [ + "MathExpressionParser.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":extensions", + ":parsing_error", + ":peekable_iterator", + ":tokenizer", + "//model/src/main/proto:math_java_proto_lite", + ], +) + kt_android_library( name = "tokenizer", srcs = [ diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt new file mode 100644 index 00000000000..80b0acd49b3 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -0,0 +1,1044 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathFunctionCall +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import org.oppia.android.app.model.Real +import org.oppia.android.util.math.MathExpressionParser.ParseContext.AlgebraicExpressionContext +import org.oppia.android.util.math.MathExpressionParser.ParseContext.NumericExpressionContext +import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError +import org.oppia.android.util.math.MathParsingError.EquationHasWrongNumberOfEqualsError +import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError +import org.oppia.android.util.math.MathParsingError.ExponentIsVariableExpressionError +import org.oppia.android.util.math.MathParsingError.ExponentTooLargeError +import org.oppia.android.util.math.MathParsingError.FunctionNameIncompleteError +import org.oppia.android.util.math.MathParsingError.GenericError +import org.oppia.android.util.math.MathParsingError.HangingSquareRootError +import org.oppia.android.util.math.MathParsingError.InvalidFunctionInUseError +import org.oppia.android.util.math.MathParsingError.MultipleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.NestedExponentsError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberAfterBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberBeforeBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NumberAfterVariableError +import org.oppia.android.util.math.MathParsingError.RedundantParenthesesForIndividualTermsError +import org.oppia.android.util.math.MathParsingError.SingleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.SpacesBetweenNumbersError +import org.oppia.android.util.math.MathParsingError.SubsequentBinaryOperatorsError +import org.oppia.android.util.math.MathParsingError.SubsequentUnaryOperatorsError +import org.oppia.android.util.math.MathParsingError.TermDividedByZeroError +import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError +import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError +import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError +import org.oppia.android.util.math.MathTokenizer.Companion.BinaryOperatorToken +import org.oppia.android.util.math.MathTokenizer.Companion.Token +import org.oppia.android.util.math.MathTokenizer.Companion.Token.DivideSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.EqualsSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.ExponentiationSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.FunctionName +import org.oppia.android.util.math.MathTokenizer.Companion.Token.IncompleteFunctionName +import org.oppia.android.util.math.MathTokenizer.Companion.Token.InvalidToken +import org.oppia.android.util.math.MathTokenizer.Companion.Token.LeftParenthesisSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.MinusSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.MultiplySymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.PlusSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.PositiveInteger +import org.oppia.android.util.math.MathTokenizer.Companion.Token.PositiveRealNumber +import org.oppia.android.util.math.MathTokenizer.Companion.Token.RightParenthesisSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.SquareRootSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.VariableName +import kotlin.math.absoluteValue + +class MathExpressionParser private constructor(private val parseContext: ParseContext) { + // TODO: + // - Add helpers to reduce overall parser length. + // - Integrate with new errors & update the routines to not rely on exceptions except in actual exceptional cases. Make sure optional errors can be disabled (for testing purposes). + // - Rename this to be a generic parser, update the public API, add documentation, remove the old classes, and split up the big test routines into actual separate tests. + + // TODO: implement specific errors. + // TODO: verify remaining GenericErrors are correct. + + // TODO: document that 'generic' means either 'numeric' or 'algebraic' (ie that the expression is syntactically the same between both grammars). + // TODO: document that one design goal is keeping the grammar for this parser as LL(1) & why. + + private fun parseGenericEquationGrammar(): MathParsingResult { + // generic_equation_grammar = generic_equation ; + return parseGenericEquation().maybeFail { equation -> + checkForLearnerErrors(equation.leftSide) ?: checkForLearnerErrors(equation.rightSide) + } + } + + private fun parseGenericExpressionGrammar(): MathParsingResult { + // generic_expression_grammar = generic_expression ; + return parseGenericExpression().maybeFail { expression -> checkForLearnerErrors(expression) } + } + + private fun parseGenericEquation(): MathParsingResult { + // algebraic_equation = generic_expression , equals_operator , generic_expression ; + + if (parseContext.hasNextTokenOfType()) { + // If equals starts the string, then there's no LHS. + return EquationMissingLhsOrRhsError.toFailure() + } + + val lhsResult = parseGenericExpression().also { + parseContext.consumeTokenOfType() + }.maybeFail { + if (!parseContext.hasMoreTokens()) { + // If there are no tokens following the equals symbol, then there's no RHS. + EquationMissingLhsOrRhsError + } else null + } + + val rhsResult = lhsResult.flatMap { parseGenericExpression() } + return lhsResult.combineWith(rhsResult) { lhs, rhs -> + MathEquation.newBuilder().apply { + leftSide = lhs + rightSide = rhs + }.build() + } + } + + private fun parseGenericExpression(): MathParsingResult { + // generic_expression = generic_add_sub_expression ; + return parseGenericAddSubExpression() + } + + private fun parseGenericAddSubExpression(): MathParsingResult { + // generic_add_sub_expression = + // generic_mult_div_expression , { generic_add_sub_expression_rhs } ; + return parseGenericBinaryExpression( + parseLhs = this::parseGenericMultDivExpression + ) { nextToken -> + // generic_add_sub_expression_rhs = + // generic_add_expression_rhs | generic_sub_expression_rhs ; + when (nextToken) { + is PlusSymbol -> BinaryOperationRhs( + operator = ADD, + rhsResult = parseGenericAddExpressionRhs() + ) + is MinusSymbol -> BinaryOperationRhs( + operator = SUBTRACT, + rhsResult = parseGenericSubExpressionRhs() + ) + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, + is ExponentiationSymbol, is FunctionName, is InvalidToken, is LeftParenthesisSymbol, + is MultiplySymbol, is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, + is IncompleteFunctionName, null -> null + } + } + } + + private fun parseGenericAddExpressionRhs(): MathParsingResult { + // generic_add_expression_rhs = plus_operator , generic_mult_div_expression ; + return parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(ADD) + } else null + }.flatMap { + parseGenericMultDivExpression() + } + } + + private fun parseGenericSubExpressionRhs(): MathParsingResult { + // generic_sub_expression_rhs = minus_operator , generic_mult_div_expression ; + return parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(SUBTRACT) + } else null + }.flatMap { + parseGenericMultDivExpression() + } + } + + private fun parseGenericMultDivExpression(): MathParsingResult { + // generic_mult_div_expression = + // generic_exp_expression , { generic_mult_div_expression_rhs } ; + return parseGenericBinaryExpression( + parseLhs = this::parseGenericExpExpression + ) { nextToken -> + // generic_mult_div_expression_rhs = + // generic_mult_expression_rhs + // | generic_div_expression_rhs + // | generic_implicit_mult_expression_rhs ; + when (nextToken) { + is MultiplySymbol -> BinaryOperationRhs( + operator = MULTIPLY, + rhsResult = parseGenericMultExpressionRhs() + ) + is DivideSymbol -> BinaryOperationRhs( + operator = DIVIDE, + rhsResult = parseGenericDivExpressionRhs() + ) + is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol -> BinaryOperationRhs( + operator = MULTIPLY, + rhsResult = parseGenericImplicitMultExpressionRhs(), + isImplicit = true + ) + is VariableName -> { + if (parseContext is AlgebraicExpressionContext) { + BinaryOperationRhs( + operator = MULTIPLY, + rhsResult = parseGenericImplicitMultExpressionRhs(), + isImplicit = true + ) + } else null + } + // Not a match to the expression. + is PositiveInteger, is PositiveRealNumber, is EqualsSymbol, is ExponentiationSymbol, + is InvalidToken, is MinusSymbol, is PlusSymbol, is RightParenthesisSymbol, + is IncompleteFunctionName, null -> null + } + } + } + + private fun parseGenericMultExpressionRhs(): MathParsingResult { + // generic_mult_expression_rhs = multiplication_operator , generic_exp_expression ; + return parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(MULTIPLY) + } else null + }.flatMap { + parseGenericExpExpression() + } + } + + private fun parseGenericDivExpressionRhs(): MathParsingResult { + // generic_div_expression_rhs = division_operator , generic_exp_expression ; + return parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(DIVIDE) + } else null + }.flatMap { + parseGenericExpExpression() + } + } + + private fun parseGenericImplicitMultExpressionRhs(): MathParsingResult { + // generic_implicit_mult_expression_rhs is either numeric_implicit_mult_expression_rhs or + // algebraic_implicit_mult_or_exp_expression_rhs depending on the current parser context. + return when (parseContext) { + is NumericExpressionContext -> parseNumericImplicitMultExpressionRhs() + is AlgebraicExpressionContext -> parseAlgebraicImplicitMultOrExpExpressionRhs() + } + } + + private fun parseNumericImplicitMultExpressionRhs(): MathParsingResult { + // numeric_implicit_mult_expression_rhs = generic_term_without_unary_without_number ; + return parseGenericTermWithoutUnaryWithoutNumber() + } + + private fun parseAlgebraicImplicitMultOrExpExpressionRhs(): MathParsingResult { + // algebraic_implicit_mult_or_exp_expression_rhs = + // generic_term_without_unary_without_number , [ generic_exp_expression_tail ] ; + val possibleLhs = parseGenericTermWithoutUnaryWithoutNumber() + return if (parseContext.hasNextTokenOfType()) { + parseGenericExpExpressionTail(possibleLhs) + } else possibleLhs + } + + private fun parseGenericExpExpression(): MathParsingResult { + // generic_exp_expression = generic_term_with_unary , [ generic_exp_expression_tail ] ; + val possibleLhs = parseGenericTermWithUnary() + return if (parseContext.hasNextTokenOfType()) { + parseGenericExpExpressionTail(possibleLhs) + } else possibleLhs + } + + // Use tail recursion so that the last exponentiation is evaluated first, and right-to-left + // associativity can be kept via backtracking. + private fun parseGenericExpExpressionTail( + lhsResult: MathParsingResult + ): MathParsingResult { + // generic_exp_expression_tail = exponentiation_operator , generic_exp_expression ; + return BinaryOperationRhs( + operator = EXPONENTIATE, + rhsResult = lhsResult.flatMap { + parseContext.consumeTokenOfType() + }.maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(EXPONENTIATE) + } else null + }.flatMap { + parseGenericExpExpression() + } + ).computeBinaryOperationExpression(lhsResult) + } + + private fun parseGenericTermWithUnary(): MathParsingResult { + // generic_term_with_unary = + // number | generic_term_without_unary_without_number | generic_plus_minus_unary_term ; + return when (val nextToken = parseContext.peekToken()) { + is MinusSymbol, is PlusSymbol -> parseGenericPlusMinusUnaryTerm() + is PositiveInteger, is PositiveRealNumber -> parseNumber().takeUnless { + parseContext.hasNextTokenOfType() || + parseContext.hasNextTokenOfType() + } ?: SpacesBetweenNumbersError.toFailure() + is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol -> + parseGenericTermWithoutUnaryWithoutNumber() + is VariableName -> { + if (parseContext is AlgebraicExpressionContext) { + parseGenericTermWithoutUnaryWithoutNumber() + } else VariableInNumericExpressionError.toFailure() + } + is DivideSymbol, is ExponentiationSymbol, is MultiplySymbol -> { + val previousToken = parseContext.getPreviousToken() + when { + previousToken is BinaryOperatorToken -> { + SubsequentBinaryOperatorsError( + operator1 = parseContext.extractSubexpression(previousToken), + operator2 = parseContext.extractSubexpression(nextToken) + ).toFailure() + } + nextToken is BinaryOperatorToken -> { + NoVariableOrNumberBeforeBinaryOperatorError( + operator = nextToken.getBinaryOperator() + ).toFailure() + } + else -> GenericError.toFailure() + } + } + is EqualsSymbol -> { + if (parseContext is AlgebraicExpressionContext && parseContext.isPartOfEquation) { + EquationHasWrongNumberOfEqualsError.toFailure() + } else GenericError.toFailure() + } + is IncompleteFunctionName -> nextToken.toFailure() + is InvalidToken -> nextToken.toFailure() + is RightParenthesisSymbol, null -> GenericError.toFailure() + } + } + + private fun parseGenericTermWithoutUnaryWithoutNumber(): MathParsingResult { + // generic_term_without_unary_without_number is either numeric_term_without_unary_without_number + // or algebraic_term_without_unary_without_number based the current parser context. + return when (parseContext) { + is NumericExpressionContext -> parseNumericTermWithoutUnaryWithoutNumber() + is AlgebraicExpressionContext -> parseAlgebraicTermWithoutUnaryWithoutNumber() + } + } + + private fun parseNumericTermWithoutUnaryWithoutNumber(): MathParsingResult { + // numeric_term_without_unary_without_number = + // generic_function_expression | generic_group_expression | generic_rooted_term ; + return when (val nextToken = parseContext.peekToken()) { + is FunctionName -> parseGenericFunctionExpression() + is LeftParenthesisSymbol -> parseGenericGroupExpression() + is SquareRootSymbol -> parseGenericRootedTerm() + is VariableName -> VariableInNumericExpressionError.toFailure() + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, + is ExponentiationSymbol, is MinusSymbol, is MultiplySymbol, is PlusSymbol, + is RightParenthesisSymbol, null -> GenericError.toFailure() + is IncompleteFunctionName -> nextToken.toFailure() + is InvalidToken -> nextToken.toFailure() + } + } + + private fun parseAlgebraicTermWithoutUnaryWithoutNumber(): MathParsingResult { + // algebraic_term_without_unary_without_number = + // generic_function_expression | generic_group_expression | generic_rooted_term | variable ; + return when (val nextToken = parseContext.peekToken()) { + is FunctionName -> parseGenericFunctionExpression() + is LeftParenthesisSymbol -> parseGenericGroupExpression() + is SquareRootSymbol -> parseGenericRootedTerm() + is VariableName -> parseVariable() + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, + is ExponentiationSymbol, is MinusSymbol, is MultiplySymbol, is PlusSymbol, + is RightParenthesisSymbol, null -> GenericError.toFailure() + is IncompleteFunctionName -> nextToken.toFailure() + is InvalidToken -> nextToken.toFailure() + } + } + + private fun parseGenericFunctionExpression(): MathParsingResult { + // generic_function_expression = function_name , left_paren , generic_expression , right_paren ; + val funcNameResult = + parseContext.consumeTokenOfType().maybeFail { functionName -> + when { + !functionName.isAllowedFunction -> InvalidFunctionInUseError(functionName.parsedName) + functionName.parsedName == "sqrt" -> null + else -> GenericError + } + }.also { + parseContext.consumeTokenOfType() + } + val argResult = funcNameResult.flatMap { parseGenericExpression() } + val rightParenResult = + argResult.flatMap { + parseContext.consumeTokenOfType { UnbalancedParenthesesError } + } + return funcNameResult.combineWith(argResult, rightParenResult) { funcName, arg, rightParen -> + MathExpression.newBuilder().apply { + parseStartIndex = funcName.startIndex + parseEndIndex = rightParen.endIndex + functionCall = MathFunctionCall.newBuilder().apply { + functionType = MathFunctionCall.FunctionType.SQUARE_ROOT + argument = arg + }.build() + }.build() + } + } + + private fun parseGenericGroupExpression(): MathParsingResult { + // generic_group_expression = left_paren , generic_expression , right_paren ; + val leftParenResult = parseContext.consumeTokenOfType() + val expResult = + leftParenResult.flatMap { + if (parseContext.hasMoreTokens()) { + parseGenericExpression() + } else UnbalancedParenthesesError.toFailure() + } + val rightParenResult = + expResult.flatMap { + parseContext.consumeTokenOfType { UnbalancedParenthesesError } + } + return leftParenResult.combineWith(expResult, rightParenResult) { leftParen, exp, rightParen -> + MathExpression.newBuilder().apply { + parseStartIndex = leftParen.startIndex + parseEndIndex = rightParen.endIndex + group = exp + }.build() + } + } + + private fun parseGenericPlusMinusUnaryTerm(): MathParsingResult { + // generic_plus_minus_unary_term = generic_negated_term | generic_positive_term ; + return when (val nextToken = parseContext.peekToken()) { + is MinusSymbol -> parseGenericNegatedTerm() + is PlusSymbol -> parseGenericPositiveTerm() + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, + is ExponentiationSymbol, is FunctionName, is LeftParenthesisSymbol, is MultiplySymbol, + is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, null -> + GenericError.toFailure() + is IncompleteFunctionName -> nextToken.toFailure() + is InvalidToken -> nextToken.toFailure() + } + } + + private fun parseGenericNegatedTerm(): MathParsingResult { + // generic_negated_term = minus_operator , generic_mult_div_expression ; + val minusResult = parseContext.consumeTokenOfType() + val expResult = minusResult.flatMap { parseGenericMultDivExpression() } + return minusResult.combineWith(expResult) { minus, op -> + MathExpression.newBuilder().apply { + parseStartIndex = minus.startIndex + parseEndIndex = op.parseEndIndex + unaryOperation = MathUnaryOperation.newBuilder().apply { + operator = NEGATE + operand = op + }.build() + }.build() + } + } + + private fun parseGenericPositiveTerm(): MathParsingResult { + // generic_positive_term = plus_operator , generic_mult_div_expression ; + val plusResult = parseContext.consumeTokenOfType() + val expResult = plusResult.flatMap { parseGenericMultDivExpression() } + return plusResult.combineWith(expResult) { plus, op -> + MathExpression.newBuilder().apply { + parseStartIndex = plus.startIndex + parseEndIndex = op.parseEndIndex + unaryOperation = MathUnaryOperation.newBuilder().apply { + operator = POSITIVE + operand = op + }.build() + }.build() + } + } + + private fun parseGenericRootedTerm(): MathParsingResult { + // generic_rooted_term = square_root_operator , generic_term_with_unary ; + val sqrtResult = + parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) HangingSquareRootError else null + } + val expResult = sqrtResult.flatMap { parseGenericTermWithUnary() } + return sqrtResult.combineWith(expResult) { sqrtSymbol, op -> + MathExpression.newBuilder().apply { + parseStartIndex = sqrtSymbol.startIndex + parseEndIndex = op.parseEndIndex + functionCall = MathFunctionCall.newBuilder().apply { + functionType = MathFunctionCall.FunctionType.SQUARE_ROOT + argument = op + }.build() + }.build() + } + } + + private fun parseNumber(): MathParsingResult { + // number = positive_real_number | positive_integer ; + return when (val nextToken = parseContext.peekToken()) { + is PositiveInteger -> { + parseContext.consumeTokenOfType().map { positiveInteger -> + MathExpression.newBuilder().apply { + parseStartIndex = positiveInteger.startIndex + parseEndIndex = positiveInteger.endIndex + constant = positiveInteger.toReal() + }.build() + } + } + is PositiveRealNumber -> { + parseContext.consumeTokenOfType().map { positiveRealNumber -> + MathExpression.newBuilder().apply { + parseStartIndex = positiveRealNumber.startIndex + parseEndIndex = positiveRealNumber.endIndex + constant = positiveRealNumber.toReal() + }.build() + } + } + is DivideSymbol, is EqualsSymbol, is ExponentiationSymbol, is FunctionName, + is LeftParenthesisSymbol, is MinusSymbol, is MultiplySymbol, is PlusSymbol, + is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, null -> + GenericError.toFailure() + is IncompleteFunctionName -> nextToken.toFailure() + is InvalidToken -> nextToken.toFailure() + } + } + + private fun parseVariable(): MathParsingResult { + val variableNameResult = + parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.allowsVariables()) GenericError else null + }.maybeFail { variableName -> + return@maybeFail if (parseContext.hasMoreTokens()) { + when (val nextToken = parseContext.peekToken()) { + is PositiveInteger -> + NumberAfterVariableError(nextToken.toReal(), variableName.parsedName) + is PositiveRealNumber -> + NumberAfterVariableError(nextToken.toReal(), variableName.parsedName) + else -> null + } + } else null + } + return variableNameResult.map { variableName -> + MathExpression.newBuilder().apply { + parseStartIndex = variableName.startIndex + parseEndIndex = variableName.endIndex + variable = variableName.parsedName + }.build() + } + } + + private fun parseGenericBinaryExpression( + parseLhs: () -> MathParsingResult, + parseRhs: (Token?) -> BinaryOperationRhs? + ): MathParsingResult { + var lastLhsResult = parseLhs() + while (!lastLhsResult.isFailure()) { + // Compute the next LHS if there are further RHS expressions. + lastLhsResult = + parseRhs(parseContext.peekToken()) + ?.computeBinaryOperationExpression(lastLhsResult) + ?: break // Not a match to the expression. + } + return lastLhsResult + } + + private fun checkForLearnerErrors(expression: MathExpression): MathParsingError? { + val firstMultiRedundantGroup = expression.findFirstMultiRedundantGroup() + val nextRedundantGroup = expression.findNextRedundantGroup() + val nextUnaryOperation = expression.findNextRedundantUnaryOperation() + val nextExpWithVariableExp = expression.findNextExponentiationWithVariablePower() + val nextExpWithTooLargePower = expression.findNextExponentiationWithTooLargePower() + val nextExpWithNestedExp = expression.findNextNestedExponentiation() + val nextDivByZero = expression.findNextDivisionByZero() + val disallowedVariables = expression.findAllDisallowedVariables(parseContext) + // Note that the order of checks here is important since errors have precedence, and some are + // redundant and, in the wrong order, may cause the wrong error to be returned. + val includeOptionalErrors = parseContext.errorCheckingMode.includesOptionalErrors() + return when { + includeOptionalErrors && firstMultiRedundantGroup != null -> { + val subExpression = parseContext.extractSubexpression(firstMultiRedundantGroup) + MultipleRedundantParenthesesError(subExpression, firstMultiRedundantGroup) + } + includeOptionalErrors && expression.expressionTypeCase == GROUP -> + SingleRedundantParenthesesError(parseContext.rawExpression, expression) + includeOptionalErrors && nextRedundantGroup != null -> { + val subExpression = parseContext.extractSubexpression(nextRedundantGroup) + RedundantParenthesesForIndividualTermsError(subExpression, nextRedundantGroup) + } + includeOptionalErrors && nextUnaryOperation != null -> SubsequentUnaryOperatorsError + includeOptionalErrors && nextExpWithVariableExp != null -> ExponentIsVariableExpressionError + includeOptionalErrors && nextExpWithTooLargePower != null -> ExponentTooLargeError + includeOptionalErrors && nextExpWithNestedExp != null -> NestedExponentsError + includeOptionalErrors && nextDivByZero != null -> TermDividedByZeroError + includeOptionalErrors && disallowedVariables.isNotEmpty() -> + DisabledVariablesInUseError(disallowedVariables.toList()) + else -> ensureNoRemainingTokens() + } + } + + private fun ensureNoRemainingTokens(): MathParsingError? { + // Make sure all tokens were consumed (otherwise there are trailing tokens which invalidate the + // whole grammar). + return if (parseContext.hasMoreTokens()) { + when (val nextToken = parseContext.peekToken()) { + is LeftParenthesisSymbol, is RightParenthesisSymbol -> UnbalancedParenthesesError + is EqualsSymbol -> { + if (parseContext is AlgebraicExpressionContext && parseContext.isPartOfEquation) { + EquationHasWrongNumberOfEqualsError + } else GenericError + } + is IncompleteFunctionName -> nextToken.toError() + is InvalidToken -> nextToken.toError() + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is ExponentiationSymbol, + is FunctionName, is MinusSymbol, is MultiplySymbol, is PlusSymbol, is SquareRootSymbol, + is VariableName, null -> GenericError + } + } else null + } + + private fun PositiveInteger.toReal(): Real = Real.newBuilder().apply { + integer = parsedValue + }.build() + + private fun PositiveRealNumber.toReal(): Real = Real.newBuilder().apply { + irrational = parsedValue + }.build() + + @Suppress("unused") // The receiver is behaving as a namespace. + private fun IncompleteFunctionName.toError(): MathParsingError = FunctionNameIncompleteError + + private fun InvalidToken.toError(): MathParsingError = + UnnecessarySymbolsError(parseContext.extractSubexpression(this)) + + private fun IncompleteFunctionName.toFailure(): MathParsingResult = toError().toFailure() + + private fun InvalidToken.toFailure(): MathParsingResult = toError().toFailure() + + private sealed class ParseContext(val rawExpression: String) { + val tokens: PeekableIterator by lazy { + PeekableIterator.fromSequence(MathTokenizer.tokenize(rawExpression)) + } + private var previousToken: Token? = null + + abstract val errorCheckingMode: ErrorCheckingMode + + abstract fun allowsVariables(): Boolean + + fun hasMoreTokens(): Boolean = tokens.hasNext() + + fun peekToken(): Token? = tokens.peek() + + /** + * Returns the last token consumed by [consumeTokenOfType], or null if none. Note: this should + * only be used for error reporting purposes, not for parsing. Using this for parsing would, in + * certain cases, allow for a non-LL(1) grammar which is against one design goal for this + * parser. + */ + fun getPreviousToken(): Token? = previousToken + + inline fun hasNextTokenOfType(): Boolean = peekToken() is T + + inline fun consumeTokenOfType( + missingError: () -> MathParsingError = { GenericError } + ): MathParsingResult { + val maybeToken = tokens.expectNextMatches { it is T } as? T + return maybeToken?.let { token -> + previousToken = token + MathParsingResult.Success(token) + } ?: missingError().toFailure() + } + + fun extractSubexpression(token: Token): String { + return rawExpression.substring(token.startIndex, token.endIndex) + } + + fun extractSubexpression(expression: MathExpression): String { + return rawExpression.substring(expression.parseStartIndex, expression.parseEndIndex) + } + + class NumericExpressionContext( + rawExpression: String, + override val errorCheckingMode: ErrorCheckingMode + ) : ParseContext(rawExpression) { + // Numeric expressions never allow variables. + override fun allowsVariables(): Boolean = false + } + + class AlgebraicExpressionContext( + rawExpression: String, + val isPartOfEquation: Boolean, + private val allowedVariables: List, + override val errorCheckingMode: ErrorCheckingMode + ) : ParseContext(rawExpression) { + fun allowsVariable(variableName: String): Boolean = variableName in allowedVariables + + override fun allowsVariables(): Boolean = true + } + } + + companion object { + enum class ErrorCheckingMode { + REQUIRED_ONLY, + ALL_ERRORS + } + + sealed class MathParsingResult { + data class Success(val result: T) : MathParsingResult() + + data class Failure(val error: MathParsingError) : MathParsingResult() + } + + fun parseNumericExpression( + rawExpression: String, + errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS + ): MathParsingResult = + createNumericParser(rawExpression, errorCheckingMode).parseGenericExpressionGrammar() + + fun parseAlgebraicExpression( + rawExpression: String, + allowedVariables: List, + errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS + ): MathParsingResult { + return createAlgebraicParser( + rawExpression, isPartOfEquation = false, allowedVariables, errorCheckingMode + ).parseGenericExpressionGrammar() + } + + fun parseAlgebraicEquation( + rawExpression: String, + allowedVariables: List, + errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS + ): MathParsingResult { + return createAlgebraicParser( + rawExpression, isPartOfEquation = true, allowedVariables, errorCheckingMode + ).parseGenericEquationGrammar() + } + + private fun createNumericParser( + rawExpression: String, + errorCheckingMode: ErrorCheckingMode + ): MathExpressionParser = + MathExpressionParser(NumericExpressionContext(rawExpression, errorCheckingMode)) + + private fun createAlgebraicParser( + rawExpression: String, + isPartOfEquation: Boolean, + allowedVariables: List, + errorCheckingMode: ErrorCheckingMode + ): MathExpressionParser { + return MathExpressionParser( + AlgebraicExpressionContext( + rawExpression, isPartOfEquation, allowedVariables, errorCheckingMode + ) + ) + } + + private fun ErrorCheckingMode.includesOptionalErrors() = this == ErrorCheckingMode.ALL_ERRORS + + private fun MathParsingError.toFailure(): MathParsingResult = + MathParsingResult.Failure(this) + + private fun MathParsingResult.isFailure() = this is MathParsingResult.Failure + + /** + * Maps [this] result to a new value. Note that this lazily uses the provided function (i.e. + * it's only used if [this] result is passing, otherwise the method will short-circuit a failure + * state so that [this] result's failure is preserved). + * + * @param operation computes a new success result given the current successful result value + * @return a new [MathParsingResult] with a successful result provided by the operation, or the + * preserved failure of [this] result + */ + private fun MathParsingResult.map( + operation: (T1) -> T2 + ): MathParsingResult = flatMap { result -> MathParsingResult.Success(operation(result)) } + + /** + * Maps [this] result to a new value. Note that this lazily uses the provided function (i.e. + * it's only used if [this] result is passing, otherwise the method will short-circuit a failure + * state so that [this] result's failure is preserved). + * + * @param operation computes a new result (either a success or failure) given the current + * successful result value + * @return a new [MathParsingResult] with either a result provided by the operation, or the + * preserved failure of [this] result + */ + private fun MathParsingResult.flatMap( + operation: (T1) -> MathParsingResult + ): MathParsingResult { + return when (this) { + is MathParsingResult.Success -> operation(result) + is MathParsingResult.Failure -> error.toFailure() + } + } + + /** + * Potentially changes [this] result into a failure based on the provided [operation]. Note that + * this function lazily uses the operation (i.e. it's only called if [this] result is in a + * passing state), and the returned result will only be in a failing state if [operation] + * returns a non-null error. + * + * @param operation computes a failure error, or null if no error was determined, given the + * current successful result value + * @return either [this] or a failing result if [operation] was called & returned a non-null + * error + */ + private fun MathParsingResult.maybeFail( + operation: (T) -> MathParsingError? + ): MathParsingResult = flatMap { result -> operation(result)?.toFailure() ?: this } + + /** + * Calls an operation if [this] operation isn't already failing, and returns a failure only if + * that operation's result is a failure (otherwise returns [this] result). This function can be + * useful to ensure that subsequent operations are successful even when those operations' + * results are never directly used. + * + * @param operation computes a new result that, when failing, will result in a failing result + * returned from this function. This is only called if [this] result is currently + * successful. + * @return either [this] (iff either this result is failing, or the result of [operation] is a + * success), or the failure returned by [operation] + */ + private fun MathParsingResult.also( + operation: () -> MathParsingResult + ): MathParsingResult = flatMap { + when (val other = operation()) { + is MathParsingResult.Success -> this + is MathParsingResult.Failure -> other.error.toFailure() + } + } + + /** + * Combines [this] result with another result, given a specific combination function. + * + * @param other the result to combine with [this] result + * @param combine computes a new value given the result from [this] and [other]. Note that this + * is only called if both results are successful, and the corresponding successful values + * are provided in-order ([this] result's value is the first parameter, and [other]'s is the + * second). + * @return either [this] result's or [other]'s failure, if either are failing, or a successful + * result containing the value computed by [combine] + */ + private fun MathParsingResult.combineWith( + other: MathParsingResult, + combine: (I1, I2) -> O, + ): MathParsingResult { + return flatMap { result -> + other.map { otherResult -> + combine(result, otherResult) + } + } + } + + /** + * Performs the same operation as the other [combineWith] function, except with three + * [MathParsingResult]s, instead. + */ + private fun MathParsingResult.combineWith( + other1: MathParsingResult, + other2: MathParsingResult, + combine: (I1, I2, I3) -> O, + ): MathParsingResult { + return flatMap { result -> + other1.flatMap { otherResult1 -> + other2.map { otherResult2 -> + combine(result, otherResult1, otherResult2) + } + } + } + } + + private data class BinaryOperationRhs( + val operator: MathBinaryOperation.Operator, + val rhsResult: MathParsingResult, + val isImplicit: Boolean = false + ) { + fun computeBinaryOperationExpression( + lhsResult: MathParsingResult + ): MathParsingResult { + return lhsResult.combineWith(rhsResult) { lhs, rhs -> + MathExpression.newBuilder().apply { + parseStartIndex = lhs.parseStartIndex + parseEndIndex = rhs.parseEndIndex + binaryOperation = MathBinaryOperation.newBuilder().apply { + operator = this@BinaryOperationRhs.operator + leftOperand = lhs + rightOperand = rhs + isImplicit = this@BinaryOperationRhs.isImplicit + }.build() + }.build() + } + } + } + + private fun MathExpression.findFirstMultiRedundantGroup(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + binaryOperation.leftOperand.findFirstMultiRedundantGroup() + ?: binaryOperation.rightOperand.findFirstMultiRedundantGroup() + } + UNARY_OPERATION -> unaryOperation.operand.findFirstMultiRedundantGroup() + FUNCTION_CALL -> functionCall.argument.findFirstMultiRedundantGroup() + GROUP -> + group.takeIf { it.expressionTypeCase == GROUP } + ?: group.findFirstMultiRedundantGroup() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findNextRedundantGroup(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + binaryOperation.leftOperand.findNextRedundantGroup() + ?: binaryOperation.rightOperand.findNextRedundantGroup() + } + UNARY_OPERATION -> unaryOperation.operand.findNextRedundantGroup() + FUNCTION_CALL -> functionCall.argument.findNextRedundantGroup() + GROUP -> group.takeIf { + it.expressionTypeCase in listOf(CONSTANT, VARIABLE) + } ?: group.findNextRedundantGroup() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findNextRedundantUnaryOperation(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + binaryOperation.leftOperand.findNextRedundantUnaryOperation() + ?: binaryOperation.rightOperand.findNextRedundantUnaryOperation() + } + UNARY_OPERATION -> unaryOperation.operand.takeIf { + it.expressionTypeCase == UNARY_OPERATION + } ?: unaryOperation.operand.findNextRedundantUnaryOperation() + FUNCTION_CALL -> functionCall.argument.findNextRedundantUnaryOperation() + GROUP -> group.findNextRedundantUnaryOperation() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findNextExponentiationWithVariablePower(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + takeIf { + binaryOperation.operator == EXPONENTIATE && + binaryOperation.rightOperand.isVariableExpression() + } ?: binaryOperation.leftOperand.findNextExponentiationWithVariablePower() + ?: binaryOperation.rightOperand.findNextExponentiationWithVariablePower() + } + UNARY_OPERATION -> unaryOperation.operand.findNextExponentiationWithVariablePower() + FUNCTION_CALL -> functionCall.argument.findNextExponentiationWithVariablePower() + GROUP -> group.findNextExponentiationWithVariablePower() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findNextExponentiationWithTooLargePower(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + takeIf { + binaryOperation.operator == EXPONENTIATE && + binaryOperation.rightOperand.expressionTypeCase == CONSTANT && + binaryOperation.rightOperand.constant.toDouble() > 5.0 + } ?: binaryOperation.leftOperand.findNextExponentiationWithTooLargePower() + ?: binaryOperation.rightOperand.findNextExponentiationWithTooLargePower() + } + UNARY_OPERATION -> unaryOperation.operand.findNextExponentiationWithTooLargePower() + FUNCTION_CALL -> functionCall.argument.findNextExponentiationWithTooLargePower() + GROUP -> group.findNextExponentiationWithTooLargePower() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findNextNestedExponentiation(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + takeIf { + binaryOperation.operator == EXPONENTIATE && + binaryOperation.rightOperand.containsExponentiation() + } ?: binaryOperation.leftOperand.findNextNestedExponentiation() + ?: binaryOperation.rightOperand.findNextNestedExponentiation() + } + UNARY_OPERATION -> unaryOperation.operand.findNextNestedExponentiation() + FUNCTION_CALL -> functionCall.argument.findNextNestedExponentiation() + GROUP -> group.findNextNestedExponentiation() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findNextDivisionByZero(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + takeIf { + binaryOperation.operator == DIVIDE && + binaryOperation.rightOperand.expressionTypeCase == CONSTANT && + binaryOperation.rightOperand.constant + .toDouble().absoluteValue.approximatelyEquals(0.0) + } ?: binaryOperation.leftOperand.findNextDivisionByZero() + ?: binaryOperation.rightOperand.findNextDivisionByZero() + } + UNARY_OPERATION -> unaryOperation.operand.findNextDivisionByZero() + FUNCTION_CALL -> functionCall.argument.findNextDivisionByZero() + GROUP -> group.findNextDivisionByZero() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findAllDisallowedVariables(context: ParseContext): Set { + return if (context is AlgebraicExpressionContext) { + findAllDisallowedVariablesAux(context) + } else setOf() + } + + private fun MathExpression.findAllDisallowedVariablesAux( + context: AlgebraicExpressionContext + ): Set { + return when (expressionTypeCase) { + VARIABLE -> if (context.allowsVariable(variable)) setOf() else setOf(variable) + BINARY_OPERATION -> { + binaryOperation.leftOperand.findAllDisallowedVariablesAux(context) + + binaryOperation.rightOperand.findAllDisallowedVariablesAux(context) + } + UNARY_OPERATION -> unaryOperation.operand.findAllDisallowedVariablesAux(context) + FUNCTION_CALL -> functionCall.argument.findAllDisallowedVariablesAux(context) + GROUP -> group.findAllDisallowedVariablesAux(context) + CONSTANT, EXPRESSIONTYPE_NOT_SET, null -> setOf() + } + } + + private fun MathExpression.isVariableExpression(): Boolean { + return when (expressionTypeCase) { + VARIABLE -> true + BINARY_OPERATION -> { + binaryOperation.leftOperand.isVariableExpression() || + binaryOperation.rightOperand.isVariableExpression() + } + UNARY_OPERATION -> unaryOperation.operand.isVariableExpression() + FUNCTION_CALL -> functionCall.argument.isVariableExpression() + GROUP -> group.isVariableExpression() + CONSTANT, EXPRESSIONTYPE_NOT_SET, null -> false + } + } + + private fun MathExpression.containsExponentiation(): Boolean { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + binaryOperation.operator == EXPONENTIATE || + binaryOperation.leftOperand.containsExponentiation() || + binaryOperation.rightOperand.containsExponentiation() + } + UNARY_OPERATION -> unaryOperation.operand.containsExponentiation() + FUNCTION_CALL -> functionCall.argument.containsExponentiation() + GROUP -> group.containsExponentiation() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> false + } + } + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt new file mode 100644 index 00000000000..44fd1debb4a --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt @@ -0,0 +1,69 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.Real + +sealed class MathParsingError { + object SpacesBetweenNumbersError : MathParsingError() + + object UnbalancedParenthesesError : MathParsingError() + + data class SingleRedundantParenthesesError( + val rawExpression: String, + val expression: MathExpression + ) : MathParsingError() + + data class MultipleRedundantParenthesesError( + val rawExpression: String, + val expression: MathExpression + ) : MathParsingError() + + data class RedundantParenthesesForIndividualTermsError( + val rawExpression: String, + val expression: MathExpression + ) : MathParsingError() + + data class UnnecessarySymbolsError(val invalidSymbol: String) : MathParsingError() + + data class NumberAfterVariableError(val number: Real, val variable: String) : MathParsingError() + + data class SubsequentBinaryOperatorsError( + val operator1: String, + val operator2: String + ) : MathParsingError() + + object SubsequentUnaryOperatorsError : MathParsingError() + + data class NoVariableOrNumberBeforeBinaryOperatorError( + val operator: MathBinaryOperation.Operator + ) : MathParsingError() + + data class NoVariableOrNumberAfterBinaryOperatorError( + val operator: MathBinaryOperation.Operator + ) : MathParsingError() + + object ExponentIsVariableExpressionError : MathParsingError() + + object ExponentTooLargeError : MathParsingError() + + object NestedExponentsError : MathParsingError() + + object HangingSquareRootError : MathParsingError() + + object TermDividedByZeroError : MathParsingError() + + object VariableInNumericExpressionError : MathParsingError() + + data class DisabledVariablesInUseError(val variables: List) : MathParsingError() + + object EquationHasWrongNumberOfEqualsError : MathParsingError() + + object EquationMissingLhsOrRhsError : MathParsingError() + + data class InvalidFunctionInUseError(val functionName: String) : MathParsingError() + + object FunctionNameIncompleteError : MathParsingError() + + object GenericError : MathParsingError() +} diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt new file mode 100644 index 00000000000..94fc6e50ab4 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt @@ -0,0 +1,227 @@ +package org.oppia.android.util.math + +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.app.model.MathEquation +import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.robolectric.annotation.LooperMode + +/** Tests for [MathExpressionParser]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class AlgebraicEquationParserTest { + @Test + fun testLotsOfCasesForAlgebraicEquation() { + expectFailureWhenParsingAlgebraicEquation(" x =") + expectFailureWhenParsingAlgebraicEquation(" = y") + + val equation1 = parseAlgebraicEquationSuccessfully("x = 1") + assertThat(equation1).hasLeftHandSideThat().hasStructureThatMatches { + variable { + withNameThat().isEqualTo("x") + } + } + + val equation2 = + parseAlgebraicEquationSuccessfully( + "y = mx + b", allowedVariables = listOf("x", "y", "b", "m") + ) + assertThat(equation2).hasLeftHandSideThat().hasStructureThatMatches { + variable { + withNameThat().isEqualTo("y") + } + } + assertThat(equation2).hasRightHandSideThat().hasStructureThatMatches { + addition { + leftOperand { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("m") + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + rightOperand { + variable { + withNameThat().isEqualTo("b") + } + } + } + } + + val equation3 = parseAlgebraicEquationSuccessfully("y = (x+1)^2") + assertThat(equation3).hasLeftHandSideThat().hasStructureThatMatches { + variable { + withNameThat().isEqualTo("y") + } + } + assertThat(equation3).hasRightHandSideThat().hasStructureThatMatches { + exponentiation { + leftOperand { + group { + addition { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + + val equation4 = parseAlgebraicEquationSuccessfully("y = (x+1)(x-1)") + assertThat(equation4).hasLeftHandSideThat().hasStructureThatMatches { + variable { + withNameThat().isEqualTo("y") + } + } + assertThat(equation4).hasRightHandSideThat().hasStructureThatMatches { + multiplication { + leftOperand { + group { + addition { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + } + } + } + + expectFailureWhenParsingAlgebraicEquation("y = (x+1)(x-1) 2") + expectFailureWhenParsingAlgebraicEquation("y 2 = (x+1)(x-1)") + + val equation5 = + parseAlgebraicEquationSuccessfully( + "a*x^2 + b*x + c = 0", allowedVariables = listOf("x", "a", "b", "c") + ) + assertThat(equation5).hasLeftHandSideThat().hasStructureThatMatches { + addition { + leftOperand { + addition { + leftOperand { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("a") + } + } + rightOperand { + exponentiation { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("b") + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + } + } + rightOperand { + variable { + withNameThat().isEqualTo("c") + } + } + } + } + assertThat(equation5).hasRightHandSideThat().hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(0) + } + } + } + + private companion object { + // TODO: fix helper API. + + private fun expectFailureWhenParsingAlgebraicEquation(expression: String): MathParsingError { + val result = parseAlgebraicEquationWithAllErrors(expression) + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error + } + + private fun parseAlgebraicEquationSuccessfully( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathEquation { + val result = parseAlgebraicEquationWithAllErrors(expression, allowedVariables) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicEquationWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicEquation( + expression, allowedVariables, ErrorCheckingMode.ALL_ERRORS + ) + } + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt new file mode 100644 index 00000000000..9fea4084970 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt @@ -0,0 +1,1939 @@ +package org.oppia.android.util.math + +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.app.model.MathExpression +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.robolectric.annotation.LooperMode + +/** Tests for [MathExpressionParser]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class AlgebraicExpressionParserTest { + @Test + fun testLotsOfCasesForAlgebraicExpression() { + // TODO: split this up + // TODO: add log string generation for expressions. + expectFailureWhenParsingAlgebraicExpression("") + + val expression1 = parseAlgebraicExpressionWithAllErrors("1") + assertThat(expression1).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + + val expression61 = parseAlgebraicExpressionWithAllErrors("x") + assertThat(expression61).hasStructureThatMatches { + variable { + withNameThat().isEqualTo("x") + } + } + + val expression2 = parseAlgebraicExpressionWithAllErrors(" 2 ") + assertThat(expression2).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + + val expression3 = parseAlgebraicExpressionWithAllErrors(" 2.5 ") + assertThat(expression3).hasStructureThatMatches { + constant { + withValueThat().isIrrationalThat().isWithin(1e-5).of(2.5) + } + } + + val expression62 = parseAlgebraicExpressionWithAllErrors(" y ") + assertThat(expression62).hasStructureThatMatches { + variable { + withNameThat().isEqualTo("y") + } + } + + val expression63 = parseAlgebraicExpressionWithAllErrors(" z x ") + assertThat(expression63).hasStructureThatMatches { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("z") + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + + val expression4 = parseAlgebraicExpressionWithoutOptionalErrors("2^3^2") + assertThat(expression4).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression23 = parseAlgebraicExpressionWithAllErrors("(2^3)^2") + assertThat(expression23).hasStructureThatMatches { + exponentiation { + leftOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + + val expression24 = parseAlgebraicExpressionWithAllErrors("512/32/4") + assertThat(expression24).hasStructureThatMatches { + division { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(512) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(32) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val expression25 = parseAlgebraicExpressionWithAllErrors("512/(32/4)") + assertThat(expression25).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(512) + } + } + rightOperand { + group { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(32) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + val expression5 = parseAlgebraicExpressionWithAllErrors("sqrt(2)") + assertThat(expression5).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + + expectFailureWhenParsingAlgebraicExpression("sqr(2)") + + val expression64 = parseAlgebraicExpressionWithoutOptionalErrors("xyz(2)") + assertThat(expression64).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + variable { + withNameThat().isEqualTo("y") + } + } + } + } + rightOperand { + variable { + withNameThat().isEqualTo("z") + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression6 = parseAlgebraicExpressionWithAllErrors("732") + assertThat(expression6).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(732) + } + } + + expectFailureWhenParsingAlgebraicExpression("73 2") + + // Verify order of operations between higher & lower precedent operators. + val expression32 = parseAlgebraicExpressionWithAllErrors("3+4^5") + assertThat(expression32).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + + val expression7 = parseAlgebraicExpressionWithoutOptionalErrors("3*2-3+4^7*8/3*2+7") + assertThat(expression7).hasStructureThatMatches { + // To better visualize the precedence & order of operations, see this grouped version: + // (((3*2)-3)+((((4^7)*8)/3)*2))+7. + addition { + leftOperand { + // ((3*2)-3)+((((4^7)*8)/3)*2) + addition { + leftOperand { + // (1*2)-3 + subtraction { + leftOperand { + // 3*2 + multiplication { + leftOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // (((4^7)*8)/3)*2 + multiplication { + leftOperand { + // ((4^7)*8)/3 + division { + leftOperand { + // (4^7)*8 + multiplication { + leftOperand { + // 4^7 + exponentiation { + leftOperand { + // 4 + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + rightOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + rightOperand { + // 8 + constant { + withValueThat().isIntegerThat().isEqualTo(8) + } + } + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + + expectFailureWhenParsingAlgebraicExpression("x = √2 × 7 ÷ 4") + + val expression8 = parseAlgebraicExpressionWithAllErrors("(1+2)(3+4)") + assertThat(expression8).hasStructureThatMatches { + multiplication { + leftOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + // Right implicit multiplication of numbers isn't allowed. + expectFailureWhenParsingAlgebraicExpression("(1+2)2") + + val expression10 = parseAlgebraicExpressionWithAllErrors("2(1+2)") + assertThat(expression10).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + // Right implicit multiplication of numbers isn't allowed. + expectFailureWhenParsingAlgebraicExpression("sqrt(2)3") + + val expression12 = parseAlgebraicExpressionWithAllErrors("3sqrt(2)") + assertThat(expression12).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression65 = parseAlgebraicExpressionWithAllErrors("xsqrt(2)") + assertThat(expression65).hasStructureThatMatches { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression13 = parseAlgebraicExpressionWithAllErrors("sqrt(2)*(1+2)*(3-2^5)") + assertThat(expression13).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + } + } + } + + val expression58 = parseAlgebraicExpressionWithAllErrors("sqrt(2)(1+2)(3-2^5)") + assertThat(expression58).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + } + } + } + + val expression14 = parseAlgebraicExpressionWithoutOptionalErrors("((3))") + assertThat(expression14).hasStructureThatMatches { + group { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val expression15 = parseAlgebraicExpressionWithoutOptionalErrors("++3") + assertThat(expression15).hasStructureThatMatches { + positive { + operand { + positive { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + + val expression16 = parseAlgebraicExpressionWithoutOptionalErrors("--4") + assertThat(expression16).hasStructureThatMatches { + negation { + operand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + val expression17 = parseAlgebraicExpressionWithAllErrors("1+-4") + assertThat(expression17).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + val expression18 = parseAlgebraicExpressionWithAllErrors("1++4") + assertThat(expression18).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + positive { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + val expression19 = parseAlgebraicExpressionWithAllErrors("1--4") + assertThat(expression19).hasStructureThatMatches { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + expectFailureWhenParsingAlgebraicExpression("1-^-4") + + val expression20 = parseAlgebraicExpressionWithAllErrors("√2 × 7 ÷ 4") + assertThat(expression20).hasStructureThatMatches { + division { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + expectFailureWhenParsingAlgebraicExpression("1+2 &asdf") + + val expression21 = parseAlgebraicExpressionWithAllErrors("sqrt(2)sqrt(3)sqrt(4)") + // Note that this tree demonstrates left associativity. + assertThat(expression21).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + val expression22 = parseAlgebraicExpressionWithAllErrors("(1+2)(3-7^2)(5+-17)") + assertThat(expression22).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + // 1+2 + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + // 3-7^2 + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + } + rightOperand { + // 5+-17 + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(17) + } + } + } + } + } + } + } + } + } + + val expression26 = parseAlgebraicExpressionWithAllErrors("3^-2") + assertThat(expression26).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression27 = parseAlgebraicExpressionWithoutOptionalErrors("(3^-2)^(3^-2)") + assertThat(expression27).hasStructureThatMatches { + exponentiation { + leftOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + rightOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + } + + val expression28 = parseAlgebraicExpressionWithAllErrors("1-3^sqrt(4)") + assertThat(expression28).hasStructureThatMatches { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + } + + // "Hard" order of operation problems loosely based on & other problems that can often stump + // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. + val expression29 = parseAlgebraicExpressionWithAllErrors("3÷2*(3+4)") + assertThat(expression29).hasStructureThatMatches { + multiplication { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + val expression59 = parseAlgebraicExpressionWithAllErrors("3÷2(3+4)") + assertThat(expression59).hasStructureThatMatches { + multiplication { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + // Numbers cannot have implicit multiplication unless they are in groups. + expectFailureWhenParsingAlgebraicExpression("2 2") + + expectFailureWhenParsingAlgebraicExpression("2 2^2") + + expectFailureWhenParsingAlgebraicExpression("2^2 2") + + val expression31 = parseAlgebraicExpressionWithoutOptionalErrors("(3)(4)(5)") + assertThat(expression31).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + + val expression33 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)") + assertThat(expression33).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + // Verify that implicit multiple has lower precedence than exponentiation. + val expression34 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)(4)") + assertThat(expression34).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + + // An exponentiation can never be an implicit right operand. + expectFailureWhenParsingAlgebraicExpression("2^(3)2^2") + + val expression35 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)*2^2") + assertThat(expression35).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + // An exponentiation can be a right operand of an implicit mult if it's grouped. + val expression36 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)(2^2)") + assertThat(expression36).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + // An exponentiation can never be an implicit right operand. + expectFailureWhenParsingAlgebraicExpression("2^3(4)2^3") + + val expression38 = parseAlgebraicExpressionWithoutOptionalErrors("2^3(4)*2^3") + assertThat(expression38).hasStructureThatMatches { + // 2^3(4)*2^3 + multiplication { + leftOperand { + // 2^3(4) + multiplication { + leftOperand { + // 2^3 + exponentiation { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // 4 + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + rightOperand { + // 2^3 + exponentiation { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + + expectFailureWhenParsingAlgebraicExpression("2^2 2^2") + expectFailureWhenParsingAlgebraicExpression("(3) 2^2") + expectFailureWhenParsingAlgebraicExpression("sqrt(3) 2^2") + expectFailureWhenParsingAlgebraicExpression("√2 2^2") + expectFailureWhenParsingAlgebraicExpression("2^2 3") + + expectFailureWhenParsingAlgebraicExpression("-2 3") + + val expression39 = parseAlgebraicExpressionWithAllErrors("-(1+2)") + assertThat(expression39).hasStructureThatMatches { + negation { + operand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + // Should pass for algebra. + val expression66 = parseAlgebraicExpressionWithAllErrors("-2 x") + assertThat(expression66).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + } + } + + val expression40 = parseAlgebraicExpressionWithAllErrors("-2 (1+2)") + assertThat(expression40).hasStructureThatMatches { + // The negation happens last for parity with other common calculators. + negation { + operand { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + } + + val expression41 = parseAlgebraicExpressionWithoutOptionalErrors("-2^3(4)") + assertThat(expression41).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + val expression43 = parseAlgebraicExpressionWithoutOptionalErrors("√2^2(3)") + assertThat(expression43).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + val expression60 = parseAlgebraicExpressionWithoutOptionalErrors("√(2^2(3))") + assertThat(expression60).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + group { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } + } + + val expression42 = parseAlgebraicExpressionWithAllErrors("-2*-2") + // Note that the following structure is not the same as (-2)*(-2) since unary negation has + // higher precedence than multiplication, so it's first & recurses to include the entire + // multiplication expression. + assertThat(expression42).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + + val expression44 = parseAlgebraicExpressionWithoutOptionalErrors("2(2)") + assertThat(expression44).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression45 = parseAlgebraicExpressionWithAllErrors("2sqrt(2)") + assertThat(expression45).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression46 = parseAlgebraicExpressionWithAllErrors("2√2") + assertThat(expression46).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression47 = parseAlgebraicExpressionWithoutOptionalErrors("(2)(2)") + assertThat(expression47).hasStructureThatMatches { + multiplication { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression48 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)(2)") + assertThat(expression48).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression49 = parseAlgebraicExpressionWithAllErrors("sqrt(2)sqrt(2)") + assertThat(expression49).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression50 = parseAlgebraicExpressionWithAllErrors("√2√2") + assertThat(expression50).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression51 = parseAlgebraicExpressionWithoutOptionalErrors("(2)(2)(2)") + assertThat(expression51).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression52 = parseAlgebraicExpressionWithAllErrors("sqrt(2)sqrt(2)sqrt(2)") + assertThat(expression52).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression53 = parseAlgebraicExpressionWithAllErrors("√2√2√2") + assertThat(expression53).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + // Should fail for algebra. + expectFailureWhenParsingAlgebraicExpression("x7") + + // Should pass for algebra. + val expression67 = parseAlgebraicExpressionWithAllErrors("2x^2y^-3") + assertThat(expression67).hasStructureThatMatches { + // 2x^2y^-3 -> (2*(x^2))*(y^(-3)) + multiplication { + // 2x^2 + leftOperand { + multiplication { + // 2 + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + // x^2 + rightOperand { + exponentiation { + // x + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + // 2 + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + // y^-3 + rightOperand { + exponentiation { + // y + leftOperand { + variable { + withNameThat().isEqualTo("y") + } + } + // -3 + rightOperand { + negation { + // 3 + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } + } + + val expression54 = parseAlgebraicExpressionWithAllErrors("2*2/-4+7*2") + assertThat(expression54).hasStructureThatMatches { + // 2*2/-4+7*2 -> ((2*2)/(-4))+(7*2) + addition { + leftOperand { + // 2*2/-4 + division { + leftOperand { + // 2*2 + multiplication { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + // -4 + negation { + // 4 + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + rightOperand { + // 7*2 + multiplication { + leftOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression55 = parseAlgebraicExpressionWithAllErrors("3/(1-2)") + assertThat(expression55).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + val expression56 = parseAlgebraicExpressionWithoutOptionalErrors("(3)/(1-2)") + assertThat(expression56).hasStructureThatMatches { + division { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + val expression57 = parseAlgebraicExpressionWithoutOptionalErrors("3/((1-2))") + assertThat(expression57).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + group { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + + // TODO: add others, including tests for malformed expressions throughout the parser & + // tokenizer. + } + + private companion object { + // TODO: fix helper API. + + private fun expectFailureWhenParsingAlgebraicExpression( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingError { + val result = + parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error + } + + private fun parseAlgebraicExpressionWithoutOptionalErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables + ) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables + ) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables, errorCheckingMode + ) + } + } +} 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 index d918580ffb9..2a8b0c295c4 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -4,6 +4,69 @@ TODO: document load("//:oppia_android_test.bzl", "oppia_android_test") +# "//testing/src/main/java/org/oppia/android/testing/math:comparable_operation_list_subject", +# "//testing/src/main/java/org/oppia/android/testing/math:fraction_subject", +# "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", +# "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", +# "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", +# "//testing/src/main/java/org/oppia/android/testing/math:real_subject", + +oppia_android_test( + name = "AlgebraicEquationParserTest", + srcs = ["AlgebraicEquationParserTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.AlgebraicEquationParserTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//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:parser", + ], +) + +oppia_android_test( + name = "AlgebraicExpressionParserTest", + srcs = ["AlgebraicExpressionParserTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.AlgebraicExpressionParserTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//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:parser", + ], +) + +oppia_android_test( + name = "MathExpressionParserTest", + srcs = ["MathExpressionParserTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.MathExpressionParserTest", + 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_extensions_truth-liteproto-extension", + "//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:parser", + ], +) + oppia_android_test( name = "MathTokenizerTest", srcs = ["MathTokenizerTest.kt"], @@ -22,6 +85,25 @@ oppia_android_test( ], ) +oppia_android_test( + name = "NumericExpressionParserTest", + srcs = ["NumericExpressionParserTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.NumericExpressionParserTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//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:parser", + ], +) + oppia_android_test( name = "RatioExtensionsTest", srcs = ["RatioExtensionsTest.kt"], diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt new file mode 100644 index 00000000000..3c50966b01e --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -0,0 +1,344 @@ +package org.oppia.android.util.math + +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.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError +import org.oppia.android.util.math.MathParsingError.EquationHasWrongNumberOfEqualsError +import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError +import org.oppia.android.util.math.MathParsingError.ExponentIsVariableExpressionError +import org.oppia.android.util.math.MathParsingError.ExponentTooLargeError +import org.oppia.android.util.math.MathParsingError.FunctionNameIncompleteError +import org.oppia.android.util.math.MathParsingError.HangingSquareRootError +import org.oppia.android.util.math.MathParsingError.InvalidFunctionInUseError +import org.oppia.android.util.math.MathParsingError.MultipleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.NestedExponentsError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberAfterBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberBeforeBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NumberAfterVariableError +import org.oppia.android.util.math.MathParsingError.RedundantParenthesesForIndividualTermsError +import org.oppia.android.util.math.MathParsingError.SingleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.SpacesBetweenNumbersError +import org.oppia.android.util.math.MathParsingError.SubsequentBinaryOperatorsError +import org.oppia.android.util.math.MathParsingError.SubsequentUnaryOperatorsError +import org.oppia.android.util.math.MathParsingError.TermDividedByZeroError +import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError +import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError +import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError +import org.robolectric.annotation.LooperMode + +/** Tests for [MathExpressionParser]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class MathExpressionParserTest { + // TODO: add high-level checks for the three types, but don't test in detail since there are + // separate suites. Also, document the separate suites' existence in this suites's KDoc. + + @Test + fun testErrorCases() { + // TODO: split up. + val failure1 = expectFailureWhenParsingNumericExpression("73 2") + assertThat(failure1).isEqualTo(SpacesBetweenNumbersError) + + val failure2 = expectFailureWhenParsingNumericExpression("(73") + assertThat(failure2).isEqualTo(UnbalancedParenthesesError) + + val failure3 = expectFailureWhenParsingNumericExpression("73)") + assertThat(failure3).isEqualTo(UnbalancedParenthesesError) + + val failure4 = expectFailureWhenParsingNumericExpression("((73)") + assertThat(failure4).isEqualTo(UnbalancedParenthesesError) + + val failure5 = expectFailureWhenParsingNumericExpression("73 (") + assertThat(failure5).isEqualTo(UnbalancedParenthesesError) + + val failure6 = expectFailureWhenParsingNumericExpression("73 )") + assertThat(failure6).isEqualTo(UnbalancedParenthesesError) + + val failure7 = expectFailureWhenParsingNumericExpression("sqrt(73") + assertThat(failure7).isEqualTo(UnbalancedParenthesesError) + + // TODO: test properties on errors (& add better testing library for errors, or at least helpers). + val failure8 = expectFailureWhenParsingNumericExpression("(7 * 2 + 4)") + assertThat(failure8).isInstanceOf(SingleRedundantParenthesesError::class.java) + + val failure9 = expectFailureWhenParsingNumericExpression("((5 + 4))") + assertThat(failure9).isInstanceOf(MultipleRedundantParenthesesError::class.java) + + val failure13 = expectFailureWhenParsingNumericExpression("(((5 + 4)))") + assertThat(failure13).isInstanceOf(MultipleRedundantParenthesesError::class.java) + + val failure14 = expectFailureWhenParsingNumericExpression("1+((5 + 4))") + assertThat(failure14).isInstanceOf(MultipleRedundantParenthesesError::class.java) + + val failure15 = expectFailureWhenParsingNumericExpression("1+(7*((( 9 + 3) )))") + assertThat(failure15).isInstanceOf(MultipleRedundantParenthesesError::class.java) + assertThat((failure15 as MultipleRedundantParenthesesError).rawExpression) + .isEqualTo("(( 9 + 3) )") + + parseNumericExpressionSuccessfully("1+(5+4)") + parseNumericExpressionSuccessfully("(5+4)+1") + + val failure10 = expectFailureWhenParsingNumericExpression("(5) + 4") + assertThat(failure10).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) + + val failure11 = expectFailureWhenParsingNumericExpression("5^(2)") + assertThat(failure11).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) + assertThat((failure11 as RedundantParenthesesForIndividualTermsError).rawExpression) + .isEqualTo("2") + + val failure12 = expectFailureWhenParsingNumericExpression("sqrt((2))") + assertThat(failure12).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) + + val failure16 = expectFailureWhenParsingNumericExpression("$2") + assertThat(failure16).isInstanceOf(UnnecessarySymbolsError::class.java) + assertThat((failure16 as UnnecessarySymbolsError).invalidSymbol).isEqualTo("$") + + val failure17 = expectFailureWhenParsingNumericExpression("5%") + assertThat(failure17).isInstanceOf(UnnecessarySymbolsError::class.java) + assertThat((failure17 as UnnecessarySymbolsError).invalidSymbol).isEqualTo("%") + + val failure18 = expectFailureWhenParsingAlgebraicExpression("x5") + assertThat(failure18).isInstanceOf(NumberAfterVariableError::class.java) + assertThat((failure18 as NumberAfterVariableError).number.integer).isEqualTo(5) + assertThat(failure18.variable).isEqualTo("x") + + val failure19 = expectFailureWhenParsingAlgebraicExpression("2+y 3.14*7") + assertThat(failure19).isInstanceOf(NumberAfterVariableError::class.java) + assertThat((failure19 as NumberAfterVariableError).number.irrational).isWithin(1e-5).of(3.14) + assertThat(failure19.variable).isEqualTo("y") + + // TODO: expand to multiple tests or use parametrized tests. + // RHS operators don't result in unary operations (which are valid in the grammar). + val rhsOperators = listOf("*", "×", "/", "÷", "^") + val lhsOperators = rhsOperators + listOf("+", "-", "−") + val operatorCombinations = lhsOperators.flatMap { op1 -> rhsOperators.map { op1 to it } } + for ((op1, op2) in operatorCombinations) { + val failure22 = expectFailureWhenParsingNumericExpression(expression = "1 $op1$op2 2") + assertThat(failure22).isInstanceOf(SubsequentBinaryOperatorsError::class.java) + assertThat((failure22 as SubsequentBinaryOperatorsError).operator1).isEqualTo(op1) + assertThat(failure22.operator2).isEqualTo(op2) + } + + val failure37 = expectFailureWhenParsingNumericExpression("++2") + assertThat(failure37).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + val failure38 = expectFailureWhenParsingAlgebraicExpression("--x") + assertThat(failure38).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + val failure39 = expectFailureWhenParsingAlgebraicExpression("-+x") + assertThat(failure39).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + val failure40 = expectFailureWhenParsingNumericExpression("+-2") + assertThat(failure40).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + parseNumericExpressionSuccessfully("2++3") // Will succeed since it's 2 + (+2). + val failure41 = expectFailureWhenParsingNumericExpression("2+++3") + assertThat(failure41).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + val failure23 = expectFailureWhenParsingNumericExpression("/2") + assertThat(failure23).isInstanceOf(NoVariableOrNumberBeforeBinaryOperatorError::class.java) + assertThat((failure23 as NoVariableOrNumberBeforeBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.DIVIDE) + + val failure24 = expectFailureWhenParsingAlgebraicExpression("*x") + assertThat(failure24).isInstanceOf(NoVariableOrNumberBeforeBinaryOperatorError::class.java) + assertThat((failure24 as NoVariableOrNumberBeforeBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.MULTIPLY) + + val failure27 = expectFailureWhenParsingNumericExpression("2^") + assertThat(failure27).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure27 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.EXPONENTIATE) + + val failure25 = expectFailureWhenParsingNumericExpression("2/") + assertThat(failure25).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure25 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.DIVIDE) + + val failure26 = expectFailureWhenParsingAlgebraicExpression("x*") + assertThat(failure26).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure26 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.MULTIPLY) + + val failure28 = expectFailureWhenParsingAlgebraicExpression("x+") + assertThat(failure28).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure28 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.ADD) + + val failure29 = expectFailureWhenParsingAlgebraicExpression("x-") + assertThat(failure29).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure29 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.SUBTRACT) + + val failure42 = expectFailureWhenParsingAlgebraicExpression("2^x") + assertThat(failure42).isInstanceOf(ExponentIsVariableExpressionError::class.java) + + val failure43 = expectFailureWhenParsingAlgebraicExpression("2^(1+x)") + assertThat(failure43).isInstanceOf(ExponentIsVariableExpressionError::class.java) + + val failure44 = expectFailureWhenParsingAlgebraicExpression("2^3^x") + assertThat(failure44).isInstanceOf(ExponentIsVariableExpressionError::class.java) + + val failure45 = expectFailureWhenParsingAlgebraicExpression("2^sqrt(x)") + assertThat(failure45).isInstanceOf(ExponentIsVariableExpressionError::class.java) + + val failure46 = expectFailureWhenParsingNumericExpression("2^7") + assertThat(failure46).isInstanceOf(ExponentTooLargeError::class.java) + + val failure47 = expectFailureWhenParsingNumericExpression("2^30.12") + assertThat(failure47).isInstanceOf(ExponentTooLargeError::class.java) + + parseNumericExpressionSuccessfully("2^3") + + val failure48 = expectFailureWhenParsingNumericExpression("2^3^2") + assertThat(failure48).isInstanceOf(NestedExponentsError::class.java) + + val failure49 = expectFailureWhenParsingAlgebraicExpression("x^2^5") + assertThat(failure49).isInstanceOf(NestedExponentsError::class.java) + + val failure20 = expectFailureWhenParsingNumericExpression("2√") + assertThat(failure20).isInstanceOf(HangingSquareRootError::class.java) + + val failure50 = expectFailureWhenParsingNumericExpression("2/0") + assertThat(failure50).isInstanceOf(TermDividedByZeroError::class.java) + + val failure51 = expectFailureWhenParsingAlgebraicExpression("x/0") + assertThat(failure51).isInstanceOf(TermDividedByZeroError::class.java) + + val failure52 = expectFailureWhenParsingNumericExpression("sqrt(2+7/0.0)") + assertThat(failure52).isInstanceOf(TermDividedByZeroError::class.java) + + val failure21 = expectFailureWhenParsingNumericExpression("x+y") + assertThat(failure21).isInstanceOf(VariableInNumericExpressionError::class.java) + + val failure53 = expectFailureWhenParsingAlgebraicExpression("x+y+a") + assertThat(failure53).isInstanceOf(DisabledVariablesInUseError::class.java) + assertThat((failure53 as DisabledVariablesInUseError).variables).containsExactly("a") + + val failure54 = expectFailureWhenParsingAlgebraicExpression("apple") + assertThat(failure54).isInstanceOf(DisabledVariablesInUseError::class.java) + assertThat((failure54 as DisabledVariablesInUseError).variables) + .containsExactly("a", "p", "l", "e") + + val failure55 = + expectFailureWhenParsingAlgebraicExpression("apple", allowedVariables = listOf("a", "p", "l")) + assertThat(failure55).isInstanceOf(DisabledVariablesInUseError::class.java) + assertThat((failure55 as DisabledVariablesInUseError).variables).containsExactly("e") + + parseAlgebraicExpressionSuccessfully("x+y+z") + + val failure56 = + expectFailureWhenParsingAlgebraicExpression("x+y+z", allowedVariables = listOf()) + assertThat(failure56).isInstanceOf(DisabledVariablesInUseError::class.java) + assertThat((failure56 as DisabledVariablesInUseError).variables).containsExactly("x", "y", "z") + + val failure30 = expectFailureWhenParsingAlgebraicEquation("x==2") + assertThat(failure30).isInstanceOf(EquationHasWrongNumberOfEqualsError::class.java) + + val failure31 = expectFailureWhenParsingAlgebraicEquation("x=2=y") + assertThat(failure31).isInstanceOf(EquationHasWrongNumberOfEqualsError::class.java) + + val failure32 = expectFailureWhenParsingAlgebraicEquation("x=2=") + assertThat(failure32).isInstanceOf(EquationHasWrongNumberOfEqualsError::class.java) + + val failure33 = expectFailureWhenParsingAlgebraicEquation("x=") + assertThat(failure33).isInstanceOf(EquationMissingLhsOrRhsError::class.java) + + val failure34 = expectFailureWhenParsingAlgebraicEquation("=x") + assertThat(failure34).isInstanceOf(EquationMissingLhsOrRhsError::class.java) + + val failure35 = expectFailureWhenParsingAlgebraicEquation("=x") + assertThat(failure35).isInstanceOf(EquationMissingLhsOrRhsError::class.java) + + // TODO: expand to multiple tests or use parametrized tests. + val prohibitedFunctionNames = + listOf( + "exp", "log", "log10", "ln", "sin", "cos", "tan", "cot", "csc", "sec", "atan", "asin", + "acos", "abs" + ) + for (functionName in prohibitedFunctionNames) { + val failure36 = expectFailureWhenParsingAlgebraicEquation("$functionName(0.5)") + assertThat(failure36).isInstanceOf(InvalidFunctionInUseError::class.java) + assertThat((failure36 as InvalidFunctionInUseError).functionName).isEqualTo(functionName) + } + + val failure57 = expectFailureWhenParsingAlgebraicExpression("sq") + assertThat(failure57).isInstanceOf(FunctionNameIncompleteError::class.java) + + val failure58 = expectFailureWhenParsingAlgebraicExpression("sqr") + assertThat(failure58).isInstanceOf(FunctionNameIncompleteError::class.java) + + // TODO: Other cases: sqrt(, sqrt(), sqrt 2, +2 + } + + private companion object { + // TODO: fix helper API. + + private fun expectFailureWhenParsingNumericExpression(expression: String): MathParsingError { + val result = parseNumericExpressionWithAllErrors(expression) + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error + } + + private fun parseNumericExpressionSuccessfully(expression: String): MathExpression { + val result = parseNumericExpressionWithAllErrors(expression) + return (result as MathParsingResult.Success).result + } + + private fun parseNumericExpressionWithAllErrors( + expression: String + ): MathParsingResult { + return MathExpressionParser.parseNumericExpression(expression, ErrorCheckingMode.ALL_ERRORS) + } + + private fun expectFailureWhenParsingAlgebraicExpression( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingError { + val result = + parseAlgebraicExpressionWithAllErrors(expression, allowedVariables) + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error + } + + private fun parseAlgebraicExpressionSuccessfully( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = parseAlgebraicExpressionWithAllErrors(expression, allowedVariables) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables, ErrorCheckingMode.ALL_ERRORS + ) + } + + private fun expectFailureWhenParsingAlgebraicEquation(expression: String): MathParsingError { + val result = parseAlgebraicEquationInternal(expression, ErrorCheckingMode.ALL_ERRORS) + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error + } + + private fun parseAlgebraicEquationInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicEquation( + expression, allowedVariables, errorCheckingMode + ) + } + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt new file mode 100644 index 00000000000..d1f9e17b47d --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -0,0 +1,1777 @@ +package org.oppia.android.util.math + +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.app.model.MathExpression +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.robolectric.annotation.LooperMode + +/** Tests for [MathExpressionParser]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class NumericExpressionParserTest { + @Test + fun testLotsOfCasesForNumericExpression() { + // TODO: split this up + // TODO: add log string generation for expressions. + expectFailureWhenParsingNumericExpression("") + + val expression1 = parseNumericExpressionWithAllErrors("1") + assertThat(expression1).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + + expectFailureWhenParsingNumericExpression("x") + + val expression2 = parseNumericExpressionWithAllErrors(" 2 ") + assertThat(expression2).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + + val expression3 = parseNumericExpressionWithAllErrors(" 2.5 ") + assertThat(expression3).hasStructureThatMatches { + constant { + withValueThat().isIrrationalThat().isWithin(1e-5).of(2.5) + } + } + + expectFailureWhenParsingNumericExpression(" x ") + + expectFailureWhenParsingNumericExpression(" z x ") + + val expression4 = parseNumericExpressionWithoutOptionalErrors("2^3^2") + assertThat(expression4).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression23 = parseNumericExpressionWithAllErrors("(2^3)^2") + assertThat(expression23).hasStructureThatMatches { + exponentiation { + leftOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + + val expression24 = parseNumericExpressionWithAllErrors("512/32/4") + assertThat(expression24).hasStructureThatMatches { + division { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(512) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(32) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val expression25 = parseNumericExpressionWithAllErrors("512/(32/4)") + assertThat(expression25).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(512) + } + } + rightOperand { + group { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(32) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + val expression5 = parseNumericExpressionWithAllErrors("sqrt(2)") + assertThat(expression5).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + + expectFailureWhenParsingNumericExpression("sqr(2)") + + expectFailureWhenParsingNumericExpression("xyz(2)") + + val expression6 = parseNumericExpressionWithAllErrors("732") + assertThat(expression6).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(732) + } + } + + // Verify order of operations between higher & lower precedent operators. + val expression32 = parseNumericExpressionWithAllErrors("3+4^5") + assertThat(expression32).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + + val expression7 = parseNumericExpressionWithoutOptionalErrors("3*2-3+4^7*8/3*2+7") + assertThat(expression7).hasStructureThatMatches { + // To better visualize the precedence & order of operations, see this grouped version: + // (((3*2)-3)+((((4^7)*8)/3)*2))+7. + addition { + leftOperand { + // ((3*2)-3)+((((4^7)*8)/3)*2) + addition { + leftOperand { + // (1*2)-3 + subtraction { + leftOperand { + // 3*2 + multiplication { + leftOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // (((4^7)*8)/3)*2 + multiplication { + leftOperand { + // ((4^7)*8)/3 + division { + leftOperand { + // (4^7)*8 + multiplication { + leftOperand { + // 4^7 + exponentiation { + leftOperand { + // 4 + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + rightOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + rightOperand { + // 8 + constant { + withValueThat().isIntegerThat().isEqualTo(8) + } + } + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + + expectFailureWhenParsingNumericExpression("x = √2 × 7 ÷ 4") + + val expression8 = parseNumericExpressionWithAllErrors("(1+2)(3+4)") + assertThat(expression8).hasStructureThatMatches { + multiplication { + leftOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + // Right implicit multiplication of numbers isn't allowed. + expectFailureWhenParsingNumericExpression("(1+2)2") + + val expression10 = parseNumericExpressionWithAllErrors("2(1+2)") + assertThat(expression10).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + // Right implicit multiplication of numbers isn't allowed. + expectFailureWhenParsingNumericExpression("sqrt(2)3") + + val expression12 = parseNumericExpressionWithAllErrors("3sqrt(2)") + assertThat(expression12).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + expectFailureWhenParsingNumericExpression("xsqrt(2)") + + val expression13 = parseNumericExpressionWithAllErrors("sqrt(2)*(1+2)*(3-2^5)") + assertThat(expression13).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + } + } + } + + val expression58 = parseNumericExpressionWithAllErrors("sqrt(2)(1+2)(3-2^5)") + assertThat(expression58).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + } + } + } + + val expression14 = parseNumericExpressionWithoutOptionalErrors("((3))") + assertThat(expression14).hasStructureThatMatches { + group { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val expression15 = parseNumericExpressionWithoutOptionalErrors("++3") + assertThat(expression15).hasStructureThatMatches { + positive { + operand { + positive { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + + val expression16 = parseNumericExpressionWithoutOptionalErrors("--4") + assertThat(expression16).hasStructureThatMatches { + negation { + operand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + val expression17 = parseNumericExpressionWithAllErrors("1+-4") + assertThat(expression17).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + val expression18 = parseNumericExpressionWithAllErrors("1++4") + assertThat(expression18).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + positive { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + val expression19 = parseNumericExpressionWithAllErrors("1--4") + assertThat(expression19).hasStructureThatMatches { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + expectFailureWhenParsingNumericExpression("1-^-4") + + val expression20 = parseNumericExpressionWithAllErrors("√2 × 7 ÷ 4") + assertThat(expression20).hasStructureThatMatches { + division { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + expectFailureWhenParsingNumericExpression("1+2 &asdf") + + val expression21 = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(3)sqrt(4)") + // Note that this tree demonstrates left associativity. + assertThat(expression21).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + val expression22 = parseNumericExpressionWithAllErrors("(1+2)(3-7^2)(5+-17)") + assertThat(expression22).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + // 1+2 + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + // 3-7^2 + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + } + rightOperand { + // 5+-17 + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(17) + } + } + } + } + } + } + } + } + } + + val expression26 = parseNumericExpressionWithAllErrors("3^-2") + assertThat(expression26).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression27 = parseNumericExpressionWithoutOptionalErrors("(3^-2)^(3^-2)") + assertThat(expression27).hasStructureThatMatches { + exponentiation { + leftOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + rightOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + } + + val expression28 = parseNumericExpressionWithAllErrors("1-3^sqrt(4)") + assertThat(expression28).hasStructureThatMatches { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + } + + // "Hard" order of operation problems loosely based on & other problems that can often stump + // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. + val expression29 = parseNumericExpressionWithAllErrors("3÷2*(3+4)") + assertThat(expression29).hasStructureThatMatches { + multiplication { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + val expression59 = parseNumericExpressionWithAllErrors("3÷2(3+4)") + assertThat(expression59).hasStructureThatMatches { + multiplication { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + // Numbers cannot have implicit multiplication unless they are in groups. + expectFailureWhenParsingNumericExpression("2 2") + + expectFailureWhenParsingNumericExpression("2 2^2") + + expectFailureWhenParsingNumericExpression("2^2 2") + + val expression31 = parseNumericExpressionWithoutOptionalErrors("(3)(4)(5)") + assertThat(expression31).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + + val expression33 = parseNumericExpressionWithoutOptionalErrors("2^(3)") + assertThat(expression33).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + // Verify that implicit multiple has lower precedence than exponentiation. + val expression34 = parseNumericExpressionWithoutOptionalErrors("2^(3)(4)") + assertThat(expression34).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + + // An exponentiation can never be an implicit right operand. + expectFailureWhenParsingNumericExpression("2^(3)2^2") + + val expression35 = parseNumericExpressionWithoutOptionalErrors("2^(3)*2^2") + assertThat(expression35).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + // An exponentiation can be a right operand of an implicit mult if it's grouped. + val expression36 = parseNumericExpressionWithoutOptionalErrors("2^(3)(2^2)") + assertThat(expression36).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + // An exponentiation can never be an implicit right operand. + expectFailureWhenParsingNumericExpression("2^3(4)2^3") + + val expression38 = parseNumericExpressionWithoutOptionalErrors("2^3(4)*2^3") + assertThat(expression38).hasStructureThatMatches { + // 2^3(4)*2^3 + multiplication { + leftOperand { + // 2^3(4) + multiplication { + leftOperand { + // 2^3 + exponentiation { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // 4 + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + rightOperand { + // 2^3 + exponentiation { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + + expectFailureWhenParsingNumericExpression("2^2 2^2") + expectFailureWhenParsingNumericExpression("(3) 2^2") + expectFailureWhenParsingNumericExpression("sqrt(3) 2^2") + expectFailureWhenParsingNumericExpression("√2 2^2") + expectFailureWhenParsingNumericExpression("2^2 3") + + expectFailureWhenParsingNumericExpression("-2 3") + + val expression39 = parseNumericExpressionWithAllErrors("-(1+2)") + assertThat(expression39).hasStructureThatMatches { + negation { + operand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + // Should pass for algebra. + expectFailureWhenParsingNumericExpression("-2 x") + + val expression40 = parseNumericExpressionWithAllErrors("-2 (1+2)") + assertThat(expression40).hasStructureThatMatches { + // The negation happens last for parity with other common calculators. + negation { + operand { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + } + + val expression41 = parseNumericExpressionWithoutOptionalErrors("-2^3(4)") + assertThat(expression41).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + val expression43 = parseNumericExpressionWithoutOptionalErrors("√2^2(3)") + assertThat(expression43).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + val expression60 = parseNumericExpressionWithoutOptionalErrors("√(2^2(3))") + assertThat(expression60).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + group { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } + } + + val expression42 = parseNumericExpressionWithAllErrors("-2*-2") + // Note that the following structure is not the same as (-2)*(-2) since unary negation has + // higher precedence than multiplication, so it's first & recurses to include the entire + // multiplication expression. + assertThat(expression42).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + + // TODO: Here & elsewhere, fix the fact that this is actually a valid use of single-term + // parentheses (there's a bug in the current error detection logic). + val expression44 = parseNumericExpressionWithoutOptionalErrors("2(2)") + assertThat(expression44).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression45 = parseNumericExpressionWithAllErrors("2sqrt(2)") + assertThat(expression45).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression46 = parseNumericExpressionWithAllErrors("2√2") + assertThat(expression46).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression47 = parseNumericExpressionWithoutOptionalErrors("(2)(2)") + assertThat(expression47).hasStructureThatMatches { + multiplication { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression48 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)(2)") + assertThat(expression48).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression49 = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(2)") + assertThat(expression49).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression50 = parseNumericExpressionWithAllErrors("√2√2") + assertThat(expression50).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression51 = parseNumericExpressionWithoutOptionalErrors("(2)(2)(2)") + assertThat(expression51).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression52 = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(2)sqrt(2)") + assertThat(expression52).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression53 = parseNumericExpressionWithAllErrors("√2√2√2") + assertThat(expression53).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + // Should fail for algebra. + expectFailureWhenParsingNumericExpression("x7") + + // Should pass for algebra. + expectFailureWhenParsingNumericExpression("2x^2") + + val expression54 = parseNumericExpressionWithAllErrors("2*2/-4+7*2") + assertThat(expression54).hasStructureThatMatches { + // 2*2/-4+7*2 -> ((2*2)/(-4))+(7*2) + addition { + leftOperand { + // 2*2/-4 + division { + leftOperand { + // 2*2 + multiplication { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + // -4 + negation { + // 4 + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + rightOperand { + // 7*2 + multiplication { + leftOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression55 = parseNumericExpressionWithAllErrors("3/(1-2)") + assertThat(expression55).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + val expression56 = parseNumericExpressionWithoutOptionalErrors("(3)/(1-2)") + assertThat(expression56).hasStructureThatMatches { + division { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + val expression57 = parseNumericExpressionWithoutOptionalErrors("3/((1-2))") + assertThat(expression57).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + group { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + + // TODO: add others, including tests for malformed expressions throughout the parser & + // tokenizer. + } + + private companion object { + // TODO: fix helper API. + + private fun expectFailureWhenParsingNumericExpression(expression: String): MathParsingError { + val result = parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error + } + + private fun parseNumericExpressionWithoutOptionalErrors(expression: String): MathExpression { + val result = + parseNumericExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY + ) + return (result as MathParsingResult.Success).result + } + + private fun parseNumericExpressionWithAllErrors(expression: String): MathExpression { + val result = + parseNumericExpressionInternal( + expression, ErrorCheckingMode.ALL_ERRORS + ) + return (result as MathParsingResult.Success).result + } + + private fun parseNumericExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode + ): MathParsingResult { + return MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) + } + } +} From 22ae591c2db2d44a5d06b9a3e894df56d0db903e Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 11:27:15 -0800 Subject: [PATCH 013/134] Add exp evaluation & LaTeX conversion support. This is mainly copied from #2173. --- .../testing/math/MathEquationSubject.kt | 11 + .../testing/math/MathExpressionSubject.kt | 30 ++ .../org/oppia/android/util/math/BUILD.bazel | 108 +++++++- .../util/math/ExpressionToLatexConverter.kt | 74 +++++ .../android/util/math/FractionExtensions.kt | 137 +++++++++ .../util/math/MathExpressionExtensions.kt | 13 + .../util/math/NumericExpressionEvaluator.kt | 70 +++++ .../oppia/android/util/math/RealExtensions.kt | 260 ++++++++++++++++++ .../util/math/AlgebraicEquationParserTest.kt | 1 + .../math/AlgebraicExpressionParserTest.kt | 70 +++++ .../org/oppia/android/util/math/BUILD.bazel | 20 ++ .../util/math/ExpressionToLatexTest.kt | 123 +++++++++ .../util/math/NumericExpressionParserTest.kt | 71 +++++ 13 files changed, 977 insertions(+), 11 deletions(-) create mode 100644 utility/src/main/java/org/oppia/android/util/math/ExpressionToLatexConverter.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/NumericExpressionEvaluator.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexTest.kt diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt index ce24e1e08cc..373b1434b0e 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt @@ -1,10 +1,13 @@ package org.oppia.android.testing.math import com.google.common.truth.FailureMetadata +import com.google.common.truth.StringSubject import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat import com.google.common.truth.extensions.proto.LiteProtoSubject import org.oppia.android.app.model.MathEquation import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat +import org.oppia.android.util.math.toRawLatex class MathEquationSubject( metadata: FailureMetadata, @@ -14,6 +17,14 @@ class MathEquationSubject( fun hasRightHandSideThat(): MathExpressionSubject = assertThat(actual.rightSide) + fun convertsToLatexStringThat(): StringSubject = + assertThat(convertToLatex(divAsFraction = false)) + + fun convertsWithFractionsToLatexStringThat(): StringSubject = + assertThat(convertToLatex(divAsFraction = true)) + + private fun convertToLatex(divAsFraction: Boolean): String = actual.toRawLatex(divAsFraction) + companion object { fun assertThat(actual: MathEquation): MathEquationSubject = assertAbout(::MathEquationSubject).that(actual) diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt index c9be134e209..eea079a7e4b 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt @@ -1,6 +1,8 @@ package org.oppia.android.testing.math +import com.google.common.truth.DoubleSubject import com.google.common.truth.FailureMetadata +import com.google.common.truth.IntegerSubject import com.google.common.truth.StringSubject import com.google.common.truth.Truth.assertAbout import com.google.common.truth.Truth.assertThat @@ -17,6 +19,8 @@ import org.oppia.android.app.model.MathFunctionCall import org.oppia.android.app.model.MathUnaryOperation import org.oppia.android.app.model.Real import org.oppia.android.testing.math.RealSubject.Companion.assertThat +import org.oppia.android.util.math.evaluateAsNumericExpression +import org.oppia.android.util.math.toRawLatex // See: https://kotlinlang.org/docs/type-safe-builders.html. class MathExpressionSubject( @@ -28,6 +32,32 @@ class MathExpressionSubject( ExpressionComparator.createFromExpression(actual).also(init) } + fun evaluatesToRationalThat(): FractionSubject = + FractionSubject.assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.RATIONAL).rational) + + fun evaluatesToIrrationalThat(): DoubleSubject = + assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.IRRATIONAL).irrational) + + fun evaluatesToIntegerThat(): IntegerSubject = + assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.INTEGER).integer) + + fun convertsToLatexStringThat(): StringSubject = + assertThat(convertToLatex(divAsFraction = false)) + + fun convertsWithFractionsToLatexStringThat(): StringSubject = + assertThat(convertToLatex(divAsFraction = true)) + + private fun evaluateAsReal(expectedType: Real.RealTypeCase): Real { + val real = actual.evaluateAsNumericExpression() + assertWithMessage("Failed to evaluate numeric expression").that(real).isNotNull() + assertWithMessage("Expected constant to evaluate to $expectedType") + .that(real?.realTypeCase) + .isEqualTo(expectedType) + return checkNotNull(real) // Just to remove the nullable operator; the actual check is above. + } + + private fun convertToLatex(divAsFraction: Boolean): String = actual.toRawLatex(divAsFraction) + // TODO: update DSL to not have return values (since it's unnecessary). @ExpressionComparatorMarker class ExpressionComparator private constructor(private val expression: MathExpression) { 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 index 1e0da7381b5..351b04e5f33 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -4,20 +4,18 @@ TODO: document load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") -kt_android_library( +android_library( name = "extensions", - srcs = [ - "FloatExtensions.kt", - "FractionExtensions.kt", - "PolynomialExtensions.kt", - "RatioExtensions.kt", - "RealExtensions.kt", - ], visibility = [ "//:oppia_api_visibility", ], - deps = [ - "//model/src/main/proto:math_java_proto_lite", + exports = [ + ":float_extensions", + ":fraction_extensions", + ":math_expression_extensions", + ":polynomial_extensions", + ":ratio_extensions", + ":real_extensions", ], ) @@ -43,9 +41,9 @@ kt_android_library( "//:oppia_testing_visibility", ], deps = [ - ":extensions", ":parsing_error", ":peekable_iterator", + ":real_extensions", ":tokenizer", "//model/src/main/proto:math_java_proto_lite", ], @@ -71,3 +69,91 @@ kt_android_library( "PeekableIterator.kt", ], ) + +kt_android_library( + name = "float_extensions", + srcs = [ + "FloatExtensions.kt", + ], + deps = [ + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "fraction_extensions", + srcs = [ + "FractionExtensions.kt", + ], + deps = [ + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "math_expression_extensions", + srcs = [ + "MathExpressionExtensions.kt", + ], + deps = [ + ":expression_to_latex_converter", + ":numeric_expression_evaluator", + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "polynomial_extensions", + srcs = [ + "PolynomialExtensions.kt", + ], + deps = [ + ":real_extensions", + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "ratio_extensions", + srcs = [ + "RatioExtensions.kt", + ], + deps = [ + ":fraction_extensions", + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "real_extensions", + srcs = [ + "RealExtensions.kt", + ], + deps = [ + ":float_extensions", + ":fraction_extensions", + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "expression_to_latex_converter", + srcs = [ + "ExpressionToLatexConverter.kt", + ], + deps = [ + ":real_extensions", + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "numeric_expression_evaluator", + srcs = [ + "NumericExpressionEvaluator.kt", + ], + deps = [ + ":real_extensions", + "//model/src/main/proto:math_java_proto_lite", + ], +) diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToLatexConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToLatexConverter.kt new file mode 100644 index 00000000000..2c108f02fa7 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToLatexConverter.kt @@ -0,0 +1,74 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathFunctionCall.FunctionType +import org.oppia.android.app.model.MathFunctionCall.FunctionType.FUNCTION_UNSPECIFIED +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator + +class ExpressionToLatexConverter private constructor() { + companion object { + fun MathEquation.convertToLatex(divAsFraction: Boolean): String { + val lhs = leftSide + val rhs = rightSide + return "${lhs.convertToLatex(divAsFraction)} = ${rhs.convertToLatex(divAsFraction)}" + } + + fun MathExpression.convertToLatex(divAsFraction: Boolean): String { + return when (expressionTypeCase) { + CONSTANT -> constant.toPlainText() + VARIABLE -> variable + BINARY_OPERATION -> { + val lhsLatex = binaryOperation.leftOperand.convertToLatex(divAsFraction) + val rhsLatex = binaryOperation.rightOperand.convertToLatex(divAsFraction) + when (binaryOperation.operator) { + ADD -> "$lhsLatex + $rhsLatex" + SUBTRACT -> "$lhsLatex - $rhsLatex" + MULTIPLY -> if (binaryOperation.isImplicit) { + "$lhsLatex$rhsLatex" + } else "$lhsLatex \\times $rhsLatex" + DIVIDE -> if (divAsFraction) { + "\\frac{$lhsLatex}{$rhsLatex}" + } else "$lhsLatex \\div $rhsLatex" + EXPONENTIATE -> "$lhsLatex ^ {$rhsLatex}" + BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> + "$lhsLatex $rhsLatex" + } + } + UNARY_OPERATION -> { + val operandLatex = unaryOperation.operand.convertToLatex(divAsFraction) + when (unaryOperation.operator) { + NEGATE -> "-$operandLatex" + POSITIVE -> "+$operandLatex" + UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> operandLatex + } + } + FUNCTION_CALL -> { + val argumentLatex = functionCall.argument.convertToLatex(divAsFraction) + when (functionCall.functionType) { + SQUARE_ROOT -> "\\sqrt{$argumentLatex}" + FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> argumentLatex + } + } + GROUP -> "(${group.convertToLatex(divAsFraction)})" + EXPRESSIONTYPE_NOT_SET, null -> "" + } + } + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index 1da9fef1857..e229fd49e40 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -1,6 +1,7 @@ package org.oppia.android.util.math import org.oppia.android.app.model.Fraction +import kotlin.math.absoluteValue /** Returns whether this fraction has a fractional component. */ fun Fraction.hasFractionalPart(): Boolean { @@ -15,6 +16,12 @@ fun Fraction.isOnlyWholeNumber(): Boolean { return !hasFractionalPart() } +/** + * Returns this fraction as a whole number. Note that this will not return a value that is + * mathematically equivalent to this fraction unless [isOnlyWholeNumber] returns true. + */ +fun Fraction.toWholeNumber(): Int = if (isNegative) -wholeNumber else wholeNumber + /** * Returns a [Double] version of this fraction. * @@ -68,6 +75,22 @@ fun Fraction.toSimplestForm(): Fraction { }.build() } +/** + * Returns this fraction in its proper form by first converting to simplest denominator, then + * extracting a whole number component. + * + * This function will properly convert a fraction whose denominator is 1 into a whole number-only + * fraction. + */ +fun Fraction.toProperForm(): Fraction { + return toSimplestForm().let { + it.toBuilder().apply { + wholeNumber = it.wholeNumber + (it.numerator / it.denominator) + numerator = it.numerator % it.denominator + }.build() + } +} + /** * Returns this fraction in an improper form (that is, with a 0 whole number and only fractional * parts). @@ -80,12 +103,126 @@ fun Fraction.toImproperForm(): Fraction { }.build() } +/** Returns the inverse improper fraction representation of this fraction. */ +fun Fraction.toInvertedImproperForm(): Fraction { + return toImproperForm().let { improper -> + improper.toBuilder().apply { + numerator = improper.denominator + denominator = improper.numerator + }.build() + } +} + /** Returns the negated form of this fraction. */ operator fun Fraction.unaryMinus(): Fraction { return toBuilder().apply { isNegative = !this@unaryMinus.isNegative }.build() } +/** Adds two fractions together and returns a new one in its proper form. */ +operator fun Fraction.plus(rhs: Fraction): Fraction { + // First, eliminate the whole number by computing improper fractions. + val leftFraction = toImproperForm() + val rightFraction = rhs.toImproperForm() + + // Second, find a common denominator and compute the new numerators. + val commonDenominator = lcm(leftFraction.denominator, rightFraction.denominator) + val leftFactor = commonDenominator / leftFraction.denominator + val rightFactor = commonDenominator / rightFraction.denominator + val leftNumerator = leftFraction.numerator * leftFactor + val rightNumerator = rightFraction.numerator * rightFactor + + // Third, determine how the numerators are combined (based on negatives) and whether the result is + // negative. + val leftNeg = leftFraction.isNegative + val rightNeg = rightFraction.isNegative + val (newNumerator, isNegative) = when { + leftNeg && rightNeg -> leftNumerator + rightNumerator to true + !leftNeg && !rightNeg -> leftNumerator + rightNumerator to false + leftNeg && !rightNeg -> + (-leftNumerator + rightNumerator).absoluteValue to (leftNumerator > rightNumerator) + !leftNeg && rightNeg -> + (leftNumerator - rightNumerator).absoluteValue to (rightNumerator > leftNumerator) + else -> throw Exception("Impossible case") + } + + // Finally, compute the new fraction and convert it to proper form to compute its whole number. + return Fraction.newBuilder().apply { + this.isNegative = isNegative + numerator = newNumerator + denominator = commonDenominator + }.build().toProperForm() +} + +/** + * Subtracts the specified fraction from this fraction and returns the result in its proper form. + */ +operator fun Fraction.minus(rhs: Fraction): Fraction { + // a - b = a + -b + return this + -rhs +} + +/** Multiples this fraction by the specified and returns the result in its proper form. */ +operator fun Fraction.times(rhs: Fraction): Fraction { + // First, convert both fractions into their improper forms. + val leftFraction = toImproperForm() + val rightFraction = rhs.toImproperForm() + + // Second, multiple the numerators and denominators piece-wise. + val newNumerator = leftFraction.numerator * rightFraction.numerator + val newDenominator = leftFraction.denominator * rightFraction.denominator + + // Third, determine negative (negative is retained if only one is negative). + val isNegative = leftFraction.isNegative xor rightFraction.isNegative + return Fraction.newBuilder().apply { + this.isNegative = isNegative + numerator = newNumerator + denominator = newDenominator + }.build().toProperForm() +} + +/** Returns the proper form of the division from this fraction by the specified fraction. */ +operator fun Fraction.div(rhs: Fraction): Fraction { + // a / b = a * b^-1 (b's inverse). + return this * rhs.toInvertedImproperForm() +} + +fun Fraction.pow(exp: Int): Fraction { + return when { + exp == 0 -> { + Fraction.newBuilder().apply { + wholeNumber = 1 + denominator = 1 + }.build() + } + exp == 1 -> this + // x^-2 == 1/(x^2). + exp < 1 -> pow(-exp).toInvertedImproperForm().toProperForm() + else -> { // i > 1 + var newValue = this + for (i in 1 until exp) newValue *= this + return newValue + } + } +} + +/** Returns the [Fraction] representation of this integer (as a whole number fraction). */ +fun Int.toWholeNumberFraction(): Fraction { + val intValue = this + return Fraction.newBuilder().apply { + isNegative = intValue < 0 + wholeNumber = kotlin.math.abs(intValue) + numerator = 0 + denominator = 1 + }.build() +} + /** Returns the greatest common divisor between two integers. */ fun gcd(x: Int, y: Int): Int { return if (y == 0) x else gcd(y, x % y) } + +/** Returns the least common multiple between two integers. */ +private fun lcm(x: Int, y: Int): Int { + // Reference: https://en.wikipedia.org/wiki/Least_common_multiple#Calculation. + return (x * y).absoluteValue / gcd(x, y) +} diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt new file mode 100644 index 00000000000..59b203a0d2d --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -0,0 +1,13 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.Real +import org.oppia.android.util.math.ExpressionToLatexConverter.Companion.convertToLatex +import org.oppia.android.util.math.NumericExpressionEvaluator.Companion.evaluate + +fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(divAsFraction) + +fun MathExpression.toRawLatex(divAsFraction: Boolean): String = convertToLatex(divAsFraction) + +fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() diff --git a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionEvaluator.kt b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionEvaluator.kt new file mode 100644 index 00000000000..766da590400 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionEvaluator.kt @@ -0,0 +1,70 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathFunctionCall +import org.oppia.android.app.model.MathFunctionCall.FunctionType +import org.oppia.android.app.model.MathFunctionCall.FunctionType.FUNCTION_UNSPECIFIED +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import org.oppia.android.app.model.Real +import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator + +class NumericExpressionEvaluator private constructor() { + companion object { + fun MathExpression.evaluate(): Real? { + return when (expressionTypeCase) { + CONSTANT -> constant + VARIABLE -> null // Variables not supported in numeric expressions. + BINARY_OPERATION -> binaryOperation.evaluate() + UNARY_OPERATION -> unaryOperation.evaluate() + FUNCTION_CALL -> functionCall.evaluate() + GROUP -> group.evaluate() + EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathBinaryOperation.evaluate(): Real? { + return when (operator) { + ADD -> rightOperand.evaluate()?.let { leftOperand.evaluate()?.plus(it) } + SUBTRACT -> rightOperand.evaluate()?.let { leftOperand.evaluate()?.minus(it) } + MULTIPLY -> rightOperand.evaluate()?.let { leftOperand.evaluate()?.times(it) } + DIVIDE -> rightOperand.evaluate()?.let { leftOperand.evaluate()?.div(it) } + EXPONENTIATE -> rightOperand.evaluate()?.let { leftOperand.evaluate()?.pow(it) } + BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> null + } + } + + private fun MathUnaryOperation.evaluate(): Real? { + return when (operator) { + NEGATE -> operand.evaluate()?.let { -it } + POSITIVE -> operand.evaluate() // '+2' is the same as just '2'. + UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> null + } + } + + private fun MathFunctionCall.evaluate(): Real? { + return when (functionType) { + SQUARE_ROOT -> argument.evaluate()?.let { sqrt(it) } + FUNCTION_UNSPECIFIED, + FunctionType.UNRECOGNIZED, + null -> null + } + } + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 6df36abd3b6..83605b05808 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -1,13 +1,17 @@ package org.oppia.android.util.math +import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.Real import org.oppia.android.app.model.Real.RealTypeCase.INTEGER import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET +import kotlin.math.pow fun Real.isRational(): Boolean = realTypeCase == RATIONAL +fun Real.isInteger(): Boolean = realTypeCase == INTEGER + fun Real.isNegative(): Boolean = when (realTypeCase) { RATIONAL -> rational.isNegative IRRATIONAL -> irrational < 0 @@ -46,8 +50,264 @@ operator fun Real.unaryMinus(): Real { } } +operator fun Real.plus(rhs: Real): Real { + return combine( + this, rhs, Fraction::plus, Fraction::plus, Fraction::plus, Double::plus, Double::plus, + Double::plus, Int::plus, Int::plus, Int::add + ) +} + +operator fun Real.minus(rhs: Real): Real { + return combine( + this, rhs, Fraction::minus, Fraction::minus, Fraction::minus, Double::minus, Double::minus, + Double::minus, Int::minus, Int::minus, Int::subtract + ) +} + +operator fun Real.times(rhs: Real): Real { + return combine( + this, rhs, Fraction::times, Fraction::times, Fraction::times, Double::times, Double::times, + Double::times, Int::times, Int::times, Int::multiply + ) +} + +operator fun Real.div(rhs: Real): Real { + return combine( + this, rhs, Fraction::div, Fraction::div, Fraction::div, Double::div, Double::div, Double::div, + Int::div, Int::div, Int::divide + ) +} + +fun Real.pow(rhs: Real): Real { + // Powers can really only be effectively done via floats or whole-number only fractions. + return when (realTypeCase) { + RATIONAL -> { + // Left-hand side is Fraction. + when (rhs.realTypeCase) { + RATIONAL -> recompute { + if (rhs.rational.isOnlyWholeNumber()) { + // The fraction can be retained. + it.setRational(rational.pow(rhs.rational.wholeNumber)) + } else { + // The fraction can't realistically be retained since it's being raised to an actual + // fraction, resulting in an irrational number. + it.setIrrational(rational.toDouble().pow(rhs.rational.toDouble())) + } + } + IRRATIONAL -> recompute { it.setIrrational(rational.pow(rhs.irrational)) } + INTEGER -> recompute { it.setRational(rational.pow(rhs.integer)) } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + } + } + IRRATIONAL -> { + // Left-hand side is a double. + when (rhs.realTypeCase) { + RATIONAL -> recompute { it.setIrrational(irrational.pow(rhs.rational)) } + IRRATIONAL -> recompute { it.setIrrational(irrational.pow(rhs.irrational)) } + INTEGER -> recompute { it.setIrrational(irrational.pow(rhs.integer)) } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + } + } + INTEGER -> { + // Left-hand side is an integer. + when (rhs.realTypeCase) { + RATIONAL -> { + if (rhs.rational.isOnlyWholeNumber()) { + // Whole number-only fractions are effectively just int^int. + integer.pow(rhs.rational.wholeNumber) + } else { + // Otherwise, raising by a fraction will result in an irrational number. + recompute { it.setIrrational(integer.toDouble().pow(rhs.rational.toDouble())) } + } + } + IRRATIONAL -> recompute { it.setIrrational(integer.toDouble().pow(rhs.irrational)) } + INTEGER -> integer.pow(rhs.integer) + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + } + } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + } +} + +fun sqrt(real: Real): Real { + return when (real.realTypeCase) { + RATIONAL -> sqrt(real.rational) + IRRATIONAL -> real.recompute { it.setIrrational(kotlin.math.sqrt(real.irrational)) } + INTEGER -> sqrt(real.integer) + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $real.") + } +} + fun abs(real: Real): Real = if (real.isNegative()) -real else real +private operator fun Double.plus(rhs: Fraction): Double = this + rhs.toDouble() +private operator fun Fraction.plus(rhs: Double): Double = toDouble() + rhs +private operator fun Fraction.plus(rhs: Int): Fraction = this + rhs.toWholeNumberFraction() +private operator fun Int.plus(rhs: Fraction): Fraction = toWholeNumberFraction() + rhs +private operator fun Double.minus(rhs: Fraction): Double = this - rhs.toDouble() +private operator fun Fraction.minus(rhs: Double): Double = toDouble() - rhs +private operator fun Fraction.minus(rhs: Int): Fraction = this - rhs.toWholeNumberFraction() +private operator fun Int.minus(rhs: Fraction): Fraction = toWholeNumberFraction() - rhs +private operator fun Double.times(rhs: Fraction): Double = this * rhs.toDouble() +private operator fun Fraction.times(rhs: Double): Double = toDouble() * rhs +private operator fun Fraction.times(rhs: Int): Fraction = this * rhs.toWholeNumberFraction() +private operator fun Int.times(rhs: Fraction): Fraction = toWholeNumberFraction() * rhs +private operator fun Double.div(rhs: Fraction): Double = this / rhs.toDouble() +private operator fun Fraction.div(rhs: Double): Double = toDouble() / rhs +private operator fun Fraction.div(rhs: Int): Fraction = this / rhs.toWholeNumberFraction() +private operator fun Int.div(rhs: Fraction): Fraction = toWholeNumberFraction() / rhs + +private fun Int.add(rhs: Int): Real = Real.newBuilder().apply { integer = this@add + rhs }.build() +private fun Int.subtract(rhs: Int): Real = Real.newBuilder().apply { + integer = this@subtract - rhs +}.build() +private fun Int.multiply(rhs: Int): Real = Real.newBuilder().apply { + integer = this@multiply * rhs +}.build() +private fun Int.divide(rhs: Int): Real = Real.newBuilder().apply { + // If rhs divides this integer, retain the integer. + val lhs = this@divide + if ((lhs % rhs) == 0) { + integer = lhs / rhs + } else { + // Otherwise, keep precision by turning the division into a fraction. + rational = Fraction.newBuilder().apply { + isNegative = (lhs < 0) xor (rhs < 0) + numerator = kotlin.math.abs(lhs) + denominator = kotlin.math.abs(rhs) + }.build() + } +}.build() + +private fun Double.pow(rhs: Fraction): Double = this.pow(rhs.toDouble()) +private fun Fraction.pow(rhs: Double): Double = toDouble().pow(rhs) + +private fun Int.pow(exp: Int): Real { + return when { + exp == 0 -> Real.newBuilder().apply { integer = 0 }.build() + exp == 1 -> Real.newBuilder().apply { integer = this@pow }.build() + exp < 0 -> Real.newBuilder().apply { rational = toWholeNumberFraction().pow(exp) }.build() + else -> { + // exp > 1 + var computed = this + for (i in 0 until exp - 1) computed *= this + Real.newBuilder().apply { integer = computed }.build() + } + } +} + +private fun sqrt(fraction: Fraction): Real { + val improper = fraction.toImproperForm() + + // Attempt to take the root of the fraction's numerator & denominator. + val numeratorRoot = sqrt(improper.numerator) + val denominatorRoot = sqrt(improper.denominator) + + // If both values stayed as integers, the original fraction can be retained. Otherwise, the + // fraction must be evaluated by performing a division. + return Real.newBuilder().apply { + if (numeratorRoot.realTypeCase == denominatorRoot.realTypeCase && numeratorRoot.isInteger()) { + val rootedFraction = Fraction.newBuilder().apply { + isNegative = improper.isNegative + numerator = numeratorRoot.integer + denominator = denominatorRoot.integer + }.build().toProperForm() + if (rootedFraction.isOnlyWholeNumber()) { + // If the fractional form doesn't need to be kept, remove it. + integer = rootedFraction.toWholeNumber() + } else { + rational = rootedFraction + } + } else { + irrational = numeratorRoot.toDouble() + } + }.build() +} + +private fun sqrt(int: Int): Real { + // First, check if the integer is a square. Reference for possible methods: + // https://www.researchgate.net/post/How-to-decide-if-a-given-number-will-have-integer-square-root-or-not. + var potentialRoot = 2 + while ((potentialRoot * potentialRoot) < int) { + potentialRoot++ + } + if (potentialRoot * potentialRoot == int) { + // There's an exact integer representation of the root. + return Real.newBuilder().apply { + integer = potentialRoot + }.build() + } + + // Otherwise, compute the irrational square root. + return Real.newBuilder().apply { + irrational = kotlin.math.sqrt(int.toDouble()) + }.build() +} + private fun Real.recompute(transform: (Real.Builder) -> Real.Builder): Real { return transform(newBuilderForType()).build() } + +// TODO: consider replacing this with inline alternatives since they'll probably be simpler. +private fun combine( + lhs: Real, + rhs: Real, + leftRationalRightRationalOp: (Fraction, Fraction) -> Fraction, + leftRationalRightIrrationalOp: (Fraction, Double) -> Double, + leftRationalRightIntegerOp: (Fraction, Int) -> Fraction, + leftIrrationalRightRationalOp: (Double, Fraction) -> Double, + leftIrrationalRightIrrationalOp: (Double, Double) -> Double, + leftIrrationalRightIntegerOp: (Double, Int) -> Double, + leftIntegerRightRationalOp: (Int, Fraction) -> Fraction, + leftIntegerRightIrrationalOp: (Int, Double) -> Double, + leftIntegerRightIntegerOp: (Int, Int) -> Real, +): Real { + return when (lhs.realTypeCase) { + RATIONAL -> { + // Left-hand side is Fraction. + when (rhs.realTypeCase) { + RATIONAL -> + lhs.recompute { it.setRational(leftRationalRightRationalOp(lhs.rational, rhs.rational)) } + IRRATIONAL -> + lhs.recompute { + it.setIrrational(leftRationalRightIrrationalOp(lhs.rational, rhs.irrational)) + } + INTEGER -> + lhs.recompute { it.setRational(leftRationalRightIntegerOp(lhs.rational, rhs.integer)) } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + } + } + IRRATIONAL -> { + // Left-hand side is a double. + when (rhs.realTypeCase) { + RATIONAL -> + lhs.recompute { + it.setIrrational(leftIrrationalRightRationalOp(lhs.irrational, rhs.rational)) + } + IRRATIONAL -> + lhs.recompute { + it.setIrrational(leftIrrationalRightIrrationalOp(lhs.irrational, rhs.irrational)) + } + INTEGER -> + lhs.recompute { + it.setIrrational(leftIrrationalRightIntegerOp(lhs.irrational, rhs.integer)) + } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + } + } + INTEGER -> { + // Left-hand side is an integer. + when (rhs.realTypeCase) { + RATIONAL -> + lhs.recompute { it.setRational(leftIntegerRightRationalOp(lhs.integer, rhs.rational)) } + IRRATIONAL -> + lhs.recompute { + it.setIrrational(leftIntegerRightIrrationalOp(lhs.integer, rhs.irrational)) + } + INTEGER -> leftIntegerRightIntegerOp(lhs.integer, rhs.integer) + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + } + } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $lhs.") + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt index 94fc6e50ab4..a2cb45387d9 100644 --- a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt @@ -25,6 +25,7 @@ class AlgebraicEquationParserTest { withNameThat().isEqualTo("x") } } + assertThat(equation1).hasRightHandSideThat().evaluatesToIntegerThat().isEqualTo(1) val equation2 = parseAlgebraicEquationSuccessfully( diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt index 9fea4084970..62c88f449a6 100644 --- a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt @@ -10,6 +10,7 @@ import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode +import kotlin.math.sqrt /** Tests for [MathExpressionParser]. */ @RunWith(AndroidJUnit4::class) @@ -27,6 +28,7 @@ class AlgebraicExpressionParserTest { withValueThat().isIntegerThat().isEqualTo(1) } } + assertThat(expression1).evaluatesToIntegerThat().isEqualTo(1) val expression61 = parseAlgebraicExpressionWithAllErrors("x") assertThat(expression61).hasStructureThatMatches { @@ -41,6 +43,7 @@ class AlgebraicExpressionParserTest { withValueThat().isIntegerThat().isEqualTo(2) } } + assertThat(expression2).evaluatesToIntegerThat().isEqualTo(2) val expression3 = parseAlgebraicExpressionWithAllErrors(" 2.5 ") assertThat(expression3).hasStructureThatMatches { @@ -48,6 +51,7 @@ class AlgebraicExpressionParserTest { withValueThat().isIrrationalThat().isWithin(1e-5).of(2.5) } } + assertThat(expression3).evaluatesToIrrationalThat().isWithin(1e-5).of(2.5) val expression62 = parseAlgebraicExpressionWithAllErrors(" y ") assertThat(expression62).hasStructureThatMatches { @@ -96,6 +100,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression4).evaluatesToIntegerThat().isEqualTo(512) val expression23 = parseAlgebraicExpressionWithAllErrors("(2^3)^2") assertThat(expression23).hasStructureThatMatches { @@ -123,6 +128,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression23).evaluatesToIntegerThat().isEqualTo(64) val expression24 = parseAlgebraicExpressionWithAllErrors("512/32/4") assertThat(expression24).hasStructureThatMatches { @@ -148,6 +154,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression24).evaluatesToIntegerThat().isEqualTo(4) val expression25 = parseAlgebraicExpressionWithAllErrors("512/(32/4)") assertThat(expression25).hasStructureThatMatches { @@ -175,6 +182,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression25).evaluatesToIntegerThat().isEqualTo(64) val expression5 = parseAlgebraicExpressionWithAllErrors("sqrt(2)") assertThat(expression5).hasStructureThatMatches { @@ -186,6 +194,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression5).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0)) expectFailureWhenParsingAlgebraicExpression("sqr(2)") @@ -231,6 +240,7 @@ class AlgebraicExpressionParserTest { withValueThat().isIntegerThat().isEqualTo(732) } } + assertThat(expression6).evaluatesToIntegerThat().isEqualTo(732) expectFailureWhenParsingAlgebraicExpression("73 2") @@ -259,6 +269,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression32).evaluatesToIntegerThat().isEqualTo(1027) val expression7 = parseAlgebraicExpressionWithoutOptionalErrors("3*2-3+4^7*8/3*2+7") assertThat(expression7).hasStructureThatMatches { @@ -356,6 +367,11 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression7) + .evaluatesToRationalThat() + .evaluatesToRealThat() + .isWithin(1e-5) + .of(87391.333333333) expectFailureWhenParsingAlgebraicExpression("x = √2 × 7 ÷ 4") @@ -396,6 +412,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression8).evaluatesToIntegerThat().isEqualTo(21) // Right implicit multiplication of numbers isn't allowed. expectFailureWhenParsingAlgebraicExpression("(1+2)2") @@ -426,6 +443,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression10).evaluatesToIntegerThat().isEqualTo(6) // Right implicit multiplication of numbers isn't allowed. expectFailureWhenParsingAlgebraicExpression("sqrt(2)3") @@ -449,6 +467,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression12).evaluatesToIrrationalThat().isWithin(1e-5).of(3.0 * sqrt(2.0)) val expression65 = parseAlgebraicExpressionWithAllErrors("xsqrt(2)") assertThat(expression65).hasStructureThatMatches { @@ -529,6 +548,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression13).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) val expression58 = parseAlgebraicExpressionWithAllErrors("sqrt(2)(1+2)(3-2^5)") assertThat(expression58).hasStructureThatMatches { @@ -589,6 +609,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression58).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) val expression14 = parseAlgebraicExpressionWithoutOptionalErrors("((3))") assertThat(expression14).hasStructureThatMatches { @@ -600,6 +621,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression14).evaluatesToIntegerThat().isEqualTo(3) val expression15 = parseAlgebraicExpressionWithoutOptionalErrors("++3") assertThat(expression15).hasStructureThatMatches { @@ -615,6 +637,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression15).evaluatesToIntegerThat().isEqualTo(3) val expression16 = parseAlgebraicExpressionWithoutOptionalErrors("--4") assertThat(expression16).hasStructureThatMatches { @@ -630,6 +653,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression16).evaluatesToIntegerThat().isEqualTo(4) val expression17 = parseAlgebraicExpressionWithAllErrors("1+-4") assertThat(expression17).hasStructureThatMatches { @@ -650,6 +674,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression17).evaluatesToIntegerThat().isEqualTo(-3) val expression18 = parseAlgebraicExpressionWithAllErrors("1++4") assertThat(expression18).hasStructureThatMatches { @@ -670,6 +695,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression18).evaluatesToIntegerThat().isEqualTo(5) val expression19 = parseAlgebraicExpressionWithAllErrors("1--4") assertThat(expression19).hasStructureThatMatches { @@ -690,6 +716,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression19).evaluatesToIntegerThat().isEqualTo(5) expectFailureWhenParsingAlgebraicExpression("1-^-4") @@ -721,6 +748,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression20).evaluatesToIrrationalThat().isWithin(1e-5).of((sqrt(2.0) * 7.0) / 4.0) expectFailureWhenParsingAlgebraicExpression("1+2 &asdf") @@ -761,6 +789,10 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression21) + .evaluatesToIrrationalThat() + .isWithin(1e-5) + .of(sqrt(2.0) * sqrt(3.0) * sqrt(4.0)) val expression22 = parseAlgebraicExpressionWithAllErrors("(1+2)(3-7^2)(5+-17)") assertThat(expression22).hasStructureThatMatches { @@ -835,6 +867,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression22).evaluatesToIntegerThat().isEqualTo(1656) val expression26 = parseAlgebraicExpressionWithAllErrors("3^-2") assertThat(expression26).hasStructureThatMatches { @@ -855,6 +888,12 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression26).evaluatesToRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(9) + } val expression27 = parseAlgebraicExpressionWithoutOptionalErrors("(3^-2)^(3^-2)") assertThat(expression27).hasStructureThatMatches { @@ -901,6 +940,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression27).evaluatesToIrrationalThat().isWithin(1e-5).of(0.78338103693) val expression28 = parseAlgebraicExpressionWithAllErrors("1-3^sqrt(4)") assertThat(expression28).hasStructureThatMatches { @@ -930,6 +970,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression28).evaluatesToIntegerThat().isEqualTo(-8) // "Hard" order of operation problems loosely based on & other problems that can often stump // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. @@ -968,6 +1009,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression29).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) val expression59 = parseAlgebraicExpressionWithAllErrors("3÷2(3+4)") assertThat(expression59).hasStructureThatMatches { @@ -1004,6 +1046,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression59).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) // Numbers cannot have implicit multiplication unless they are in groups. expectFailureWhenParsingAlgebraicExpression("2 2") @@ -1042,6 +1085,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression31).evaluatesToIntegerThat().isEqualTo(60) val expression33 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)") assertThat(expression33).hasStructureThatMatches { @@ -1060,6 +1104,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression33).evaluatesToIntegerThat().isEqualTo(8) // Verify that implicit multiple has lower precedence than exponentiation. val expression34 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)(4)") @@ -1090,6 +1135,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression34).evaluatesToIntegerThat().isEqualTo(32) // An exponentiation can never be an implicit right operand. expectFailureWhenParsingAlgebraicExpression("2^(3)2^2") @@ -1129,6 +1175,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression35).evaluatesToIntegerThat().isEqualTo(32) // An exponentiation can be a right operand of an implicit mult if it's grouped. val expression36 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)(2^2)") @@ -1168,6 +1215,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression36).evaluatesToIntegerThat().isEqualTo(32) // An exponentiation can never be an implicit right operand. expectFailureWhenParsingAlgebraicExpression("2^3(4)2^3") @@ -1225,6 +1273,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression38).evaluatesToIntegerThat().isEqualTo(256) expectFailureWhenParsingAlgebraicExpression("2^2 2^2") expectFailureWhenParsingAlgebraicExpression("(3) 2^2") @@ -1255,6 +1304,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression39).evaluatesToIntegerThat().isEqualTo(-3) // Should pass for algebra. val expression66 = parseAlgebraicExpressionWithAllErrors("-2 x") @@ -1308,6 +1358,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression40).evaluatesToIntegerThat().isEqualTo(-6) val expression41 = parseAlgebraicExpressionWithoutOptionalErrors("-2^3(4)") assertThat(expression41).hasStructureThatMatches { @@ -1339,6 +1390,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression41).evaluatesToIntegerThat().isEqualTo(-32) val expression43 = parseAlgebraicExpressionWithoutOptionalErrors("√2^2(3)") assertThat(expression43).hasStructureThatMatches { @@ -1370,6 +1422,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression43).evaluatesToIrrationalThat().isWithin(1e-5).of(6.0) val expression60 = parseAlgebraicExpressionWithoutOptionalErrors("√(2^2(3))") assertThat(expression60).hasStructureThatMatches { @@ -1403,6 +1456,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression60).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(12.0)) val expression42 = parseAlgebraicExpressionWithAllErrors("-2*-2") // Note that the following structure is not the same as (-2)*(-2) since unary negation has @@ -1430,6 +1484,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression42).evaluatesToIntegerThat().isEqualTo(4) val expression44 = parseAlgebraicExpressionWithoutOptionalErrors("2(2)") assertThat(expression44).hasStructureThatMatches { @@ -1448,6 +1503,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression44).evaluatesToIntegerThat().isEqualTo(4) val expression45 = parseAlgebraicExpressionWithAllErrors("2sqrt(2)") assertThat(expression45).hasStructureThatMatches { @@ -1468,6 +1524,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression45).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) val expression46 = parseAlgebraicExpressionWithAllErrors("2√2") assertThat(expression46).hasStructureThatMatches { @@ -1488,6 +1545,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression46).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) val expression47 = parseAlgebraicExpressionWithoutOptionalErrors("(2)(2)") assertThat(expression47).hasStructureThatMatches { @@ -1508,6 +1566,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression47).evaluatesToIntegerThat().isEqualTo(4) val expression48 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)(2)") assertThat(expression48).hasStructureThatMatches { @@ -1530,6 +1589,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression48).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) val expression49 = parseAlgebraicExpressionWithAllErrors("sqrt(2)sqrt(2)") assertThat(expression49).hasStructureThatMatches { @@ -1554,6 +1614,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression49).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) val expression50 = parseAlgebraicExpressionWithAllErrors("√2√2") assertThat(expression50).hasStructureThatMatches { @@ -1578,6 +1639,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression50).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) val expression51 = parseAlgebraicExpressionWithoutOptionalErrors("(2)(2)(2)") assertThat(expression51).hasStructureThatMatches { @@ -1609,6 +1671,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression51).evaluatesToIntegerThat().isEqualTo(8) val expression52 = parseAlgebraicExpressionWithAllErrors("sqrt(2)sqrt(2)sqrt(2)") assertThat(expression52).hasStructureThatMatches { @@ -1646,6 +1709,8 @@ class AlgebraicExpressionParserTest { } } } + val sqrt2 = sqrt(2.0) + assertThat(expression52).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) val expression53 = parseAlgebraicExpressionWithAllErrors("√2√2√2") assertThat(expression53).hasStructureThatMatches { @@ -1683,6 +1748,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression53).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) // Should fail for algebra. expectFailureWhenParsingAlgebraicExpression("x7") @@ -1801,6 +1867,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression54).evaluatesToIntegerThat().isEqualTo(13) val expression55 = parseAlgebraicExpressionWithAllErrors("3/(1-2)") assertThat(expression55).hasStructureThatMatches { @@ -1828,6 +1895,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression55).evaluatesToIntegerThat().isEqualTo(-3) val expression56 = parseAlgebraicExpressionWithoutOptionalErrors("(3)/(1-2)") assertThat(expression56).hasStructureThatMatches { @@ -1857,6 +1925,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression56).evaluatesToIntegerThat().isEqualTo(-3) val expression57 = parseAlgebraicExpressionWithoutOptionalErrors("3/((1-2))") assertThat(expression57).hasStructureThatMatches { @@ -1886,6 +1955,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression57).evaluatesToIntegerThat().isEqualTo(-3) // TODO: add others, including tests for malformed expressions throughout the parser & // tokenizer. 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 index 2a8b0c295c4..282038acd19 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -49,6 +49,26 @@ oppia_android_test( ], ) +oppia_android_test( + name = "ExpressionToLatexTest", + srcs = ["ExpressionToLatexTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.ExpressionToLatexTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", + "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//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:parser", + ], +) + oppia_android_test( name = "MathExpressionParserTest", srcs = ["MathExpressionParserTest.kt"], diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexTest.kt new file mode 100644 index 00000000000..e56db9a7500 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexTest.kt @@ -0,0 +1,123 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat +import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.robolectric.annotation.LooperMode + +/** Tests for [MathExpressionParser]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class ExpressionToLatexTest { + @Test + fun testLatex() { + // TODO: split up & move to separate test suites. Finish test cases. + + val exp1 = parseNumericExpressionWithAllErrors("1") + assertThat(exp1).convertsToLatexStringThat().isEqualTo("1") + + val exp2 = parseNumericExpressionWithAllErrors("1+2") + assertThat(exp2).convertsToLatexStringThat().isEqualTo("1 + 2") + + val exp3 = parseNumericExpressionWithAllErrors("1*2") + assertThat(exp3).convertsToLatexStringThat().isEqualTo("1 \\times 2") + + val exp4 = parseNumericExpressionWithAllErrors("1/2") + assertThat(exp4).convertsToLatexStringThat().isEqualTo("1 \\div 2") + + val exp5 = parseNumericExpressionWithAllErrors("1/2") + assertThat(exp5).convertsWithFractionsToLatexStringThat().isEqualTo("\\frac{1}{2}") + + val exp10 = parseNumericExpressionWithAllErrors("√2") + assertThat(exp10).convertsToLatexStringThat().isEqualTo("\\sqrt{2}") + + val exp11 = parseNumericExpressionWithAllErrors("√(1/2)") + assertThat(exp11).convertsToLatexStringThat().isEqualTo("\\sqrt{(1 \\div 2)}") + + val exp6 = parseAlgebraicExpressionWithAllErrors("x+y") + assertThat(exp6).convertsToLatexStringThat().isEqualTo("x + y") + + val exp7 = parseAlgebraicExpressionWithoutOptionalErrors("x^(1/y)") + assertThat(exp7).convertsToLatexStringThat().isEqualTo("x ^ {(1 \\div y)}") + + val exp8 = parseAlgebraicExpressionWithoutOptionalErrors("x^(1/y)") + assertThat(exp8).convertsWithFractionsToLatexStringThat().isEqualTo("x ^ {(\\frac{1}{y})}") + + val exp9 = parseAlgebraicExpressionWithoutOptionalErrors("x^y^z") + assertThat(exp9).convertsWithFractionsToLatexStringThat().isEqualTo("x ^ {y ^ {z}}") + + val eq1 = + parseAlgebraicEquationWithAllErrors( + "7a^2+b^2+c^2=0", allowedVariables = listOf("a", "b", "c") + ) + assertThat(eq1).convertsToLatexStringThat().isEqualTo("7a ^ {2} + b ^ {2} + c ^ {2} = 0") + + val eq2 = parseAlgebraicEquationWithAllErrors("sqrt(1+x)/x=1") + assertThat(eq2).convertsToLatexStringThat().isEqualTo("\\sqrt{1 + x} \\div x = 1") + + val eq3 = parseAlgebraicEquationWithAllErrors("sqrt(1+x)/x=1") + assertThat(eq3) + .convertsWithFractionsToLatexStringThat() + .isEqualTo("\\frac{\\sqrt{1 + x}}{x} = 1") + } + + private companion object { + // TODO: fix helper API. + + private fun parseNumericExpressionWithAllErrors(expression: String): MathExpression { + val result = + MathExpressionParser.parseNumericExpression(expression, ErrorCheckingMode.ALL_ERRORS) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionWithoutOptionalErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables + ) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables + ) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables, errorCheckingMode + ) + } + + private fun parseAlgebraicEquationWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathEquation { + val result = + MathExpressionParser.parseAlgebraicEquation( + expression, allowedVariables, + ErrorCheckingMode.ALL_ERRORS + ) + return (result as MathParsingResult.Success).result + } + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index d1f9e17b47d..2dacceff76e 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -10,11 +10,13 @@ import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode +import kotlin.math.sqrt /** Tests for [MathExpressionParser]. */ @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class NumericExpressionParserTest { + @Test fun testLotsOfCasesForNumericExpression() { // TODO: split this up @@ -27,6 +29,7 @@ class NumericExpressionParserTest { withValueThat().isIntegerThat().isEqualTo(1) } } + assertThat(expression1).evaluatesToIntegerThat().isEqualTo(1) expectFailureWhenParsingNumericExpression("x") @@ -36,6 +39,7 @@ class NumericExpressionParserTest { withValueThat().isIntegerThat().isEqualTo(2) } } + assertThat(expression2).evaluatesToIntegerThat().isEqualTo(2) val expression3 = parseNumericExpressionWithAllErrors(" 2.5 ") assertThat(expression3).hasStructureThatMatches { @@ -43,6 +47,7 @@ class NumericExpressionParserTest { withValueThat().isIrrationalThat().isWithin(1e-5).of(2.5) } } + assertThat(expression3).evaluatesToIrrationalThat().isWithin(1e-5).of(2.5) expectFailureWhenParsingNumericExpression(" x ") @@ -72,6 +77,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression4).evaluatesToIntegerThat().isEqualTo(512) val expression23 = parseNumericExpressionWithAllErrors("(2^3)^2") assertThat(expression23).hasStructureThatMatches { @@ -99,6 +105,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression23).evaluatesToIntegerThat().isEqualTo(64) val expression24 = parseNumericExpressionWithAllErrors("512/32/4") assertThat(expression24).hasStructureThatMatches { @@ -124,6 +131,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression24).evaluatesToIntegerThat().isEqualTo(4) val expression25 = parseNumericExpressionWithAllErrors("512/(32/4)") assertThat(expression25).hasStructureThatMatches { @@ -151,6 +159,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression25).evaluatesToIntegerThat().isEqualTo(64) val expression5 = parseNumericExpressionWithAllErrors("sqrt(2)") assertThat(expression5).hasStructureThatMatches { @@ -162,6 +171,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression5).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0)) expectFailureWhenParsingNumericExpression("sqr(2)") @@ -173,6 +183,7 @@ class NumericExpressionParserTest { withValueThat().isIntegerThat().isEqualTo(732) } } + assertThat(expression6).evaluatesToIntegerThat().isEqualTo(732) // Verify order of operations between higher & lower precedent operators. val expression32 = parseNumericExpressionWithAllErrors("3+4^5") @@ -199,6 +210,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression32).evaluatesToIntegerThat().isEqualTo(1027) val expression7 = parseNumericExpressionWithoutOptionalErrors("3*2-3+4^7*8/3*2+7") assertThat(expression7).hasStructureThatMatches { @@ -296,6 +308,11 @@ class NumericExpressionParserTest { } } } + assertThat(expression7) + .evaluatesToRationalThat() + .evaluatesToRealThat() + .isWithin(1e-5) + .of(87391.333333333) expectFailureWhenParsingNumericExpression("x = √2 × 7 ÷ 4") @@ -336,6 +353,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression8).evaluatesToIntegerThat().isEqualTo(21) // Right implicit multiplication of numbers isn't allowed. expectFailureWhenParsingNumericExpression("(1+2)2") @@ -366,6 +384,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression10).evaluatesToIntegerThat().isEqualTo(6) // Right implicit multiplication of numbers isn't allowed. expectFailureWhenParsingNumericExpression("sqrt(2)3") @@ -389,6 +408,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression12).evaluatesToIrrationalThat().isWithin(1e-5).of(3.0 * sqrt(2.0)) expectFailureWhenParsingNumericExpression("xsqrt(2)") @@ -451,6 +471,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression13).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) val expression58 = parseNumericExpressionWithAllErrors("sqrt(2)(1+2)(3-2^5)") assertThat(expression58).hasStructureThatMatches { @@ -511,6 +532,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression58).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) val expression14 = parseNumericExpressionWithoutOptionalErrors("((3))") assertThat(expression14).hasStructureThatMatches { @@ -522,6 +544,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression14).evaluatesToIntegerThat().isEqualTo(3) val expression15 = parseNumericExpressionWithoutOptionalErrors("++3") assertThat(expression15).hasStructureThatMatches { @@ -537,6 +560,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression15).evaluatesToIntegerThat().isEqualTo(3) val expression16 = parseNumericExpressionWithoutOptionalErrors("--4") assertThat(expression16).hasStructureThatMatches { @@ -552,6 +576,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression16).evaluatesToIntegerThat().isEqualTo(4) val expression17 = parseNumericExpressionWithAllErrors("1+-4") assertThat(expression17).hasStructureThatMatches { @@ -572,6 +597,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression17).evaluatesToIntegerThat().isEqualTo(-3) val expression18 = parseNumericExpressionWithAllErrors("1++4") assertThat(expression18).hasStructureThatMatches { @@ -592,6 +618,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression18).evaluatesToIntegerThat().isEqualTo(5) val expression19 = parseNumericExpressionWithAllErrors("1--4") assertThat(expression19).hasStructureThatMatches { @@ -612,6 +639,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression19).evaluatesToIntegerThat().isEqualTo(5) expectFailureWhenParsingNumericExpression("1-^-4") @@ -643,6 +671,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression20).evaluatesToIrrationalThat().isWithin(1e-5).of((sqrt(2.0) * 7.0) / 4.0) expectFailureWhenParsingNumericExpression("1+2 &asdf") @@ -683,6 +712,10 @@ class NumericExpressionParserTest { } } } + assertThat(expression21) + .evaluatesToIrrationalThat() + .isWithin(1e-5) + .of(sqrt(2.0) * sqrt(3.0) * sqrt(4.0)) val expression22 = parseNumericExpressionWithAllErrors("(1+2)(3-7^2)(5+-17)") assertThat(expression22).hasStructureThatMatches { @@ -757,6 +790,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression22).evaluatesToIntegerThat().isEqualTo(1656) val expression26 = parseNumericExpressionWithAllErrors("3^-2") assertThat(expression26).hasStructureThatMatches { @@ -777,6 +811,12 @@ class NumericExpressionParserTest { } } } + assertThat(expression26).evaluatesToRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(9) + } val expression27 = parseNumericExpressionWithoutOptionalErrors("(3^-2)^(3^-2)") assertThat(expression27).hasStructureThatMatches { @@ -823,6 +863,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression27).evaluatesToIrrationalThat().isWithin(1e-5).of(0.78338103693) val expression28 = parseNumericExpressionWithAllErrors("1-3^sqrt(4)") assertThat(expression28).hasStructureThatMatches { @@ -852,6 +893,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression28).evaluatesToIntegerThat().isEqualTo(-8) // "Hard" order of operation problems loosely based on & other problems that can often stump // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. @@ -890,6 +932,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression29).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) val expression59 = parseNumericExpressionWithAllErrors("3÷2(3+4)") assertThat(expression59).hasStructureThatMatches { @@ -926,6 +969,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression59).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) // Numbers cannot have implicit multiplication unless they are in groups. expectFailureWhenParsingNumericExpression("2 2") @@ -964,6 +1008,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression31).evaluatesToIntegerThat().isEqualTo(60) val expression33 = parseNumericExpressionWithoutOptionalErrors("2^(3)") assertThat(expression33).hasStructureThatMatches { @@ -982,6 +1027,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression33).evaluatesToIntegerThat().isEqualTo(8) // Verify that implicit multiple has lower precedence than exponentiation. val expression34 = parseNumericExpressionWithoutOptionalErrors("2^(3)(4)") @@ -1012,6 +1058,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression34).evaluatesToIntegerThat().isEqualTo(32) // An exponentiation can never be an implicit right operand. expectFailureWhenParsingNumericExpression("2^(3)2^2") @@ -1051,6 +1098,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression35).evaluatesToIntegerThat().isEqualTo(32) // An exponentiation can be a right operand of an implicit mult if it's grouped. val expression36 = parseNumericExpressionWithoutOptionalErrors("2^(3)(2^2)") @@ -1090,6 +1138,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression36).evaluatesToIntegerThat().isEqualTo(32) // An exponentiation can never be an implicit right operand. expectFailureWhenParsingNumericExpression("2^3(4)2^3") @@ -1147,6 +1196,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression38).evaluatesToIntegerThat().isEqualTo(256) expectFailureWhenParsingNumericExpression("2^2 2^2") expectFailureWhenParsingNumericExpression("(3) 2^2") @@ -1177,6 +1227,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression39).evaluatesToIntegerThat().isEqualTo(-3) // Should pass for algebra. expectFailureWhenParsingNumericExpression("-2 x") @@ -1212,6 +1263,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression40).evaluatesToIntegerThat().isEqualTo(-6) val expression41 = parseNumericExpressionWithoutOptionalErrors("-2^3(4)") assertThat(expression41).hasStructureThatMatches { @@ -1243,6 +1295,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression41).evaluatesToIntegerThat().isEqualTo(-32) val expression43 = parseNumericExpressionWithoutOptionalErrors("√2^2(3)") assertThat(expression43).hasStructureThatMatches { @@ -1274,6 +1327,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression43).evaluatesToIrrationalThat().isWithin(1e-5).of(6.0) val expression60 = parseNumericExpressionWithoutOptionalErrors("√(2^2(3))") assertThat(expression60).hasStructureThatMatches { @@ -1307,6 +1361,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression60).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(12.0)) val expression42 = parseNumericExpressionWithAllErrors("-2*-2") // Note that the following structure is not the same as (-2)*(-2) since unary negation has @@ -1334,6 +1389,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression42).evaluatesToIntegerThat().isEqualTo(4) // TODO: Here & elsewhere, fix the fact that this is actually a valid use of single-term // parentheses (there's a bug in the current error detection logic). @@ -1354,6 +1410,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression44).evaluatesToIntegerThat().isEqualTo(4) val expression45 = parseNumericExpressionWithAllErrors("2sqrt(2)") assertThat(expression45).hasStructureThatMatches { @@ -1374,6 +1431,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression45).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) val expression46 = parseNumericExpressionWithAllErrors("2√2") assertThat(expression46).hasStructureThatMatches { @@ -1394,6 +1452,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression46).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) val expression47 = parseNumericExpressionWithoutOptionalErrors("(2)(2)") assertThat(expression47).hasStructureThatMatches { @@ -1414,6 +1473,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression47).evaluatesToIntegerThat().isEqualTo(4) val expression48 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)(2)") assertThat(expression48).hasStructureThatMatches { @@ -1436,6 +1496,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression48).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) val expression49 = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(2)") assertThat(expression49).hasStructureThatMatches { @@ -1460,6 +1521,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression49).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) val expression50 = parseNumericExpressionWithAllErrors("√2√2") assertThat(expression50).hasStructureThatMatches { @@ -1484,6 +1546,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression50).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) val expression51 = parseNumericExpressionWithoutOptionalErrors("(2)(2)(2)") assertThat(expression51).hasStructureThatMatches { @@ -1515,6 +1578,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression51).evaluatesToIntegerThat().isEqualTo(8) val expression52 = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(2)sqrt(2)") assertThat(expression52).hasStructureThatMatches { @@ -1552,6 +1616,8 @@ class NumericExpressionParserTest { } } } + val sqrt2 = sqrt(2.0) + assertThat(expression52).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) val expression53 = parseNumericExpressionWithAllErrors("√2√2√2") assertThat(expression53).hasStructureThatMatches { @@ -1589,6 +1655,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression53).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) // Should fail for algebra. expectFailureWhenParsingNumericExpression("x7") @@ -1652,6 +1719,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression54).evaluatesToIntegerThat().isEqualTo(13) val expression55 = parseNumericExpressionWithAllErrors("3/(1-2)") assertThat(expression55).hasStructureThatMatches { @@ -1679,6 +1747,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression55).evaluatesToIntegerThat().isEqualTo(-3) val expression56 = parseNumericExpressionWithoutOptionalErrors("(3)/(1-2)") assertThat(expression56).hasStructureThatMatches { @@ -1708,6 +1777,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression56).evaluatesToIntegerThat().isEqualTo(-3) val expression57 = parseNumericExpressionWithoutOptionalErrors("3/((1-2))") assertThat(expression57).hasStructureThatMatches { @@ -1737,6 +1807,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression57).evaluatesToIntegerThat().isEqualTo(-3) // TODO: add others, including tests for malformed expressions throughout the parser & // tokenizer. From d9d4963fc292d23bb765a95170b1fe9bae10f855 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 11:29:25 -0800 Subject: [PATCH 014/134] Remove unneeded comment lines. --- .../src/test/java/org/oppia/android/util/math/BUILD.bazel | 7 ------- 1 file changed, 7 deletions(-) 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 index 2a8b0c295c4..c4791dffc69 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -4,13 +4,6 @@ TODO: document load("//:oppia_android_test.bzl", "oppia_android_test") -# "//testing/src/main/java/org/oppia/android/testing/math:comparable_operation_list_subject", -# "//testing/src/main/java/org/oppia/android/testing/math:fraction_subject", -# "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", -# "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", -# "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", -# "//testing/src/main/java/org/oppia/android/testing/math:real_subject", - oppia_android_test( name = "AlgebraicEquationParserTest", srcs = ["AlgebraicEquationParserTest.kt"], From 1a8a8e85ce1c15f0e3565d10aa1392136ee94e7f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 13:39:57 -0800 Subject: [PATCH 015/134] Add expr->comparable operation list conv support. This enables the ability to compare two expressions such that operation associativity and commutativity is considered (i.e. items can be rearranged using those rules without breaking expression equality). This is mostly copied from #2173. --- .../org/oppia/android/util/math/BUILD.bazel | 21 + .../android/util/math/ComparatorExtensions.kt | 57 + ...ssionToComparableOperationListConverter.kt | 300 +++ .../util/math/MathExpressionExtensions.kt | 35 + .../oppia/android/util/math/RealExtensions.kt | 2 + .../org/oppia/android/util/math/BUILD.bazel | 19 + ...ExpressionToComparableOperationListTest.kt | 1637 +++++++++++++++++ 7 files changed, 2071 insertions(+) create mode 100644 utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListTest.kt 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 index 351b04e5f33..fbd04084430 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -10,6 +10,7 @@ android_library( "//:oppia_api_visibility", ], exports = [ + ":comparator_extensions", ":float_extensions", ":fraction_extensions", ":math_expression_extensions", @@ -70,6 +71,13 @@ kt_android_library( ], ) +kt_android_library( + name = "comparator_extensions", + srcs = [ + "ComparatorExtensions.kt", + ], +) + kt_android_library( name = "float_extensions", srcs = [ @@ -96,6 +104,7 @@ kt_android_library( "MathExpressionExtensions.kt", ], deps = [ + ":expression_to_comparable_operation_list_converter", ":expression_to_latex_converter", ":numeric_expression_evaluator", "//model/src/main/proto:math_java_proto_lite", @@ -136,6 +145,18 @@ kt_android_library( ], ) +kt_android_library( + name = "expression_to_comparable_operation_list_converter", + srcs = [ + "ExpressionToComparableOperationListConverter.kt", + ], + deps = [ + ":comparator_extensions", + ":real_extensions", + "//model/src/main/proto:math_java_proto_lite", + ], +) + kt_android_library( name = "expression_to_latex_converter", srcs = [ diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt new file mode 100644 index 00000000000..284ec69fe69 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt @@ -0,0 +1,57 @@ +package org.oppia.android.util.math + +import java.util.SortedSet + +fun comparingDeferred( + keySelector: (T) -> U, + comparatorSelector: () -> Comparator +): Comparator { + // Store as captured val for memoization. + val comparator by lazy { comparatorSelector() } + return Comparator.comparing(keySelector) { o1, o2 -> + comparator.compare(o1, o2) + } +} + +fun > Comparator.thenComparingReversed( + keySelector: (T) -> U +): Comparator = thenComparing(Comparator.comparing(keySelector).reversed()) + +fun > Comparator.thenSelectAmong( + enumSelector: (T) -> E, + vararg comparators: Pair> +): Comparator { + val comparatorMap = comparators.toMap() + return thenComparing( + Comparator { o1, o2 -> + val enum1 = enumSelector(o1) + val enum2 = enumSelector(o2) + check(enum1 == enum2) { + "Expected objects to have the same enum values: $o1 ($enum1), $o2 ($enum2)" + } + val comparator = + checkNotNull(comparatorMap[enum1]) { "No comparator for matched enum: $enum1" } + return@Comparator comparator.compare(o1, o2) + } + ) +} + +fun Comparator.toSetComparator(): Comparator> { + val itemComparator = this + return Comparator { first, second -> + // Reference: https://stackoverflow.com/a/30107086. + val firstIter = first.iterator() + val secondIter = second.iterator() + while (firstIter.hasNext() && secondIter.hasNext()) { + val comparison = itemComparator.compare(firstIter.next(), secondIter.next()) + if (comparison != 0) return@Comparator comparison // Found a different item. + } + + // Everything is equal up to here, see if the lists are different length. + return@Comparator when { + firstIter.hasNext() -> 1 // The first list is longer, therefore "greater." + secondIter.hasNext() -> -1 // Ditto, but for the second list. + else -> 0 // Otherwise, they're the same length with all equal items (and are thus equal). + } + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt new file mode 100644 index 00000000000..4cfa9acec66 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt @@ -0,0 +1,300 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.ACCUMULATION_TYPE_UNSPECIFIED +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.PRODUCT +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.SUMMATION +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.COMPARISONTYPE_NOT_SET +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.CONSTANT_TERM +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.NON_COMMUTATIVE_OPERATION +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.VARIABLE_TERM +import org.oppia.android.app.model.ComparableOperationList.NonCommutativeOperation +import org.oppia.android.app.model.ComparableOperationList.NonCommutativeOperation.OperationTypeCase +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathFunctionCall.FunctionType +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator + +class ExpressionToComparableOperationListConverter private constructor() { + companion object { + private val COMPARABLE_OPERATION_COMPARATOR: Comparator by lazy { + // Some of the comparators must be deferred since they indirectly reference this comparator + // (which isn't valid until it's fully assembled). + Comparator.comparing(ComparableOperation::getComparisonTypeCase) + .thenComparing(ComparableOperation::getIsNegated) + .thenComparing(ComparableOperation::getIsInverted) + .thenSelectAmong( + ComparableOperation::getComparisonTypeCase, + ComparableOperation.ComparisonTypeCase.COMMUTATIVE_ACCUMULATION to comparingDeferred( + ComparableOperation::getCommutativeAccumulation + ) { COMMUTATIVE_ACCUMULATION_COMPARATOR }, + NON_COMMUTATIVE_OPERATION to comparingDeferred( + ComparableOperation::getNonCommutativeOperation + ) { NON_COMMUTATIVE_OPERATION_COMPARATOR }, + CONSTANT_TERM to Comparator.comparing( + ComparableOperation::getConstantTerm, REAL_COMPARATOR + ), + VARIABLE_TERM to Comparator.comparing(ComparableOperation::getVariableTerm) + ) + } + + private val COMMUTATIVE_ACCUMULATION_COMPARATOR: Comparator by lazy { + Comparator.comparing(CommutativeAccumulation::getAccumulationType) + .thenComparing( + { accumulation -> + accumulation.combinedOperationsList.toSortedSet(COMPARABLE_OPERATION_COMPARATOR) + }, + COMPARABLE_OPERATION_COMPARATOR.toSetComparator() + ) + } + + private val NON_COMMUTATIVE_BINARY_OPERATION_COMPARATOR by lazy { + Comparator.comparing( + NonCommutativeOperation.BinaryOperation::getLeftOperand, COMPARABLE_OPERATION_COMPARATOR + ).thenComparing( + NonCommutativeOperation.BinaryOperation::getRightOperand, COMPARABLE_OPERATION_COMPARATOR + ) + } + + private val NON_COMMUTATIVE_OPERATION_COMPARATOR: Comparator by lazy { + Comparator.comparing(NonCommutativeOperation::getOperationTypeCase) + .thenSelectAmong( + NonCommutativeOperation::getOperationTypeCase, + OperationTypeCase.EXPONENTIATION to Comparator.comparing( + NonCommutativeOperation::getExponentiation, NON_COMMUTATIVE_BINARY_OPERATION_COMPARATOR + ), + OperationTypeCase.SQUARE_ROOT to Comparator.comparing( + NonCommutativeOperation::getSquareRoot, COMPARABLE_OPERATION_COMPARATOR + ), + ) + } + + fun MathExpression.toComparable(): ComparableOperationList { + return ComparableOperationList.newBuilder().apply { + rootOperation = toComparableOperation().stabilizeNegation().sort() + }.build() + } + + private fun MathExpression.toComparableOperation(): ComparableOperation { + return when (expressionTypeCase) { + CONSTANT -> ComparableOperation.newBuilder().apply { + constantTerm = constant + }.build() + VARIABLE -> ComparableOperation.newBuilder().apply { + variableTerm = variable + }.build() + BINARY_OPERATION -> when (binaryOperation.operator) { + ADD -> toSummation(isRhsNegative = false) + SUBTRACT -> toSummation(isRhsNegative = true) + MULTIPLY -> toProduct(isRhsInverted = false) + DIVIDE -> toProduct(isRhsInverted = true) + EXPONENTIATE -> + toNonCommutativeOperation(NonCommutativeOperation.Builder::setExponentiation) + BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> + ComparableOperation.getDefaultInstance() + } + UNARY_OPERATION -> when (unaryOperation.operator) { + NEGATE -> unaryOperation.operand.toComparableOperation().makeNegative() + POSITIVE -> unaryOperation.operand.toComparableOperation() + UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> + ComparableOperation.getDefaultInstance() + } + FUNCTION_CALL -> when (functionCall.functionType) { + SQUARE_ROOT -> ComparableOperation.newBuilder().apply { + nonCommutativeOperation = NonCommutativeOperation.newBuilder().apply { + squareRoot = functionCall.argument.toComparableOperation() + }.build() + }.build() + FunctionType.FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> + ComparableOperation.getDefaultInstance() + } + GROUP -> group.toComparableOperation() + EXPRESSIONTYPE_NOT_SET, null -> ComparableOperation.getDefaultInstance() + } + } + + private fun MathExpression.toSummation(isRhsNegative: Boolean): ComparableOperation { + return ComparableOperation.newBuilder().apply { + commutativeAccumulation = CommutativeAccumulation.newBuilder().apply { + accumulationType = SUMMATION + addOperationToSum(binaryOperation.leftOperand, forceNegative = false) + addOperationToSum(binaryOperation.rightOperand, forceNegative = isRhsNegative) + }.build() + }.build() + } + + private fun MathExpression.toProduct(isRhsInverted: Boolean): ComparableOperation { + return ComparableOperation.newBuilder().apply { + commutativeAccumulation = CommutativeAccumulation.newBuilder().apply { + accumulationType = PRODUCT + addOperationToProduct(binaryOperation.leftOperand, forceInverse = false) + addOperationToProduct(binaryOperation.rightOperand, forceInverse = isRhsInverted) + }.build() + }.build() + } + + private fun CommutativeAccumulation.Builder.addOperationToSum( + expression: MathExpression, + forceNegative: Boolean + ) { + when (expression.binaryOperation.operator) { + ADD -> { + // If the whole operation is negative, carry it to the left-hand side of the operation. + addOperationToSum(expression.binaryOperation.leftOperand, forceNegative) + addOperationToSum(expression.binaryOperation.rightOperand, forceNegative = false) + } + SUBTRACT -> { + addOperationToSum(expression.binaryOperation.leftOperand, forceNegative) + addOperationToSum(expression.binaryOperation.rightOperand, forceNegative = true) + } + else -> if (forceNegative) { + addCombinedOperations(expression.toComparableOperation().makeNegative()) + } else addCombinedOperations(expression.toComparableOperation()) + } + } + + private fun CommutativeAccumulation.Builder.addOperationToProduct( + expression: MathExpression, + forceInverse: Boolean + ) { + when (expression.binaryOperation.operator) { + MULTIPLY -> { + // If the whole operation is inverted, carry it to the left-hand side of the operation. + addOperationToProduct(expression.binaryOperation.leftOperand, forceInverse) + addOperationToProduct(expression.binaryOperation.rightOperand, forceInverse = false) + } + DIVIDE -> { + addOperationToProduct(expression.binaryOperation.leftOperand, forceInverse) + addOperationToProduct(expression.binaryOperation.rightOperand, forceInverse = true) + } + else -> if (forceInverse) { + addCombinedOperations(expression.toComparableOperation().makeInverted()) + } else addCombinedOperations(expression.toComparableOperation()) + } + } + + private fun MathExpression.toNonCommutativeOperation( + setOperation: NonCommutativeOperation.Builder.( + NonCommutativeOperation.BinaryOperation + ) -> NonCommutativeOperation.Builder + ): ComparableOperation { + return ComparableOperation.newBuilder().apply { + nonCommutativeOperation = NonCommutativeOperation.newBuilder().apply { + setOperation( + NonCommutativeOperation.BinaryOperation.newBuilder().apply { + leftOperand = binaryOperation.leftOperand.toComparableOperation() + rightOperand = binaryOperation.rightOperand.toComparableOperation() + }.build() + ) + }.build() + }.build() + } + + private fun ComparableOperation.makePositive(): ComparableOperation = + toBuilder().apply { isNegated = false }.build() + + private fun ComparableOperation.makeNegative(): ComparableOperation = + toBuilder().apply { isNegated = true }.build() + + private fun ComparableOperation.makeInverted(): ComparableOperation = + toBuilder().apply { isInverted = true }.build() + + private fun ComparableOperation.stabilizeNegation(): ComparableOperation { + return when (comparisonTypeCase) { + ComparableOperation.ComparisonTypeCase.COMMUTATIVE_ACCUMULATION -> { + val stabilizedOperations = + commutativeAccumulation.combinedOperationsList.map { it.stabilizeNegation() } + when (commutativeAccumulation.accumulationType) { + SUMMATION -> toBuilder().apply { + commutativeAccumulation = commutativeAccumulation.toBuilder().apply { + clearCombinedOperations() + addAllCombinedOperations(stabilizedOperations) + }.build() + }.build() + PRODUCT -> { + // Negations can be combined for all constituent operations & brought up to the + // top-level operation. + val negativeCount = stabilizedOperations.count { + it.isNegated + } + if (isNegated) 1 else 0 + val positiveOperations = stabilizedOperations.map { it.makePositive() } + toBuilder().apply { + isNegated = (negativeCount % 2) == 1 + commutativeAccumulation = commutativeAccumulation.toBuilder().apply { + clearCombinedOperations() + addAllCombinedOperations(positiveOperations) + }.build() + }.build() + } + ACCUMULATION_TYPE_UNSPECIFIED, AccumulationType.UNRECOGNIZED, null -> this + } + } + NON_COMMUTATIVE_OPERATION -> toBuilder().apply { + // Negation can't be extracted from commutative operations. + nonCommutativeOperation = when (nonCommutativeOperation.operationTypeCase) { + OperationTypeCase.EXPONENTIATION -> nonCommutativeOperation.toBuilder().apply { + exponentiation = nonCommutativeOperation.exponentiation.toBuilder().apply { + leftOperand = nonCommutativeOperation.exponentiation.leftOperand.stabilizeNegation() + rightOperand = + nonCommutativeOperation.exponentiation.rightOperand.stabilizeNegation() + }.build() + }.build() + OperationTypeCase.SQUARE_ROOT -> nonCommutativeOperation.toBuilder().apply { + squareRoot = nonCommutativeOperation.squareRoot.stabilizeNegation() + }.build() + OperationTypeCase.OPERATIONTYPE_NOT_SET, null -> nonCommutativeOperation + } + }.build() + CONSTANT_TERM -> this + VARIABLE_TERM -> this + COMPARISONTYPE_NOT_SET, null -> this + } + } + + private fun ComparableOperation.sort(): ComparableOperation { + return when (comparisonTypeCase) { + ComparableOperation.ComparisonTypeCase.COMMUTATIVE_ACCUMULATION -> toBuilder().apply { + commutativeAccumulation = commutativeAccumulation.toBuilder().apply { + clearCombinedOperations() + // Sort the operations themselves before sorting them relative to each other. + val innerSortedList = commutativeAccumulation.combinedOperationsList.map { it.sort() } + addAllCombinedOperations(innerSortedList.sortedWith(COMPARABLE_OPERATION_COMPARATOR)) + }.build() + }.build() + NON_COMMUTATIVE_OPERATION -> toBuilder().apply { + nonCommutativeOperation = when (nonCommutativeOperation.operationTypeCase) { + OperationTypeCase.EXPONENTIATION -> nonCommutativeOperation.toBuilder().apply { + exponentiation = nonCommutativeOperation.exponentiation.toBuilder().apply { + leftOperand = nonCommutativeOperation.exponentiation.leftOperand.sort() + rightOperand = nonCommutativeOperation.exponentiation.rightOperand.sort() + }.build() + }.build() + OperationTypeCase.SQUARE_ROOT -> nonCommutativeOperation.toBuilder().apply { + squareRoot = nonCommutativeOperation.squareRoot.sort() + }.build() + OperationTypeCase.OPERATIONTYPE_NOT_SET, null -> nonCommutativeOperation + } + }.build() + CONSTANT_TERM, VARIABLE_TERM, COMPARISONTYPE_NOT_SET, null -> this + } + } + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 59b203a0d2d..70fa465e30a 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -1,8 +1,17 @@ package org.oppia.android.util.math +import org.oppia.android.app.model.ComparableOperationList import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE import org.oppia.android.app.model.Real +import org.oppia.android.util.math.ExpressionToComparableOperationListConverter.Companion.toComparable import org.oppia.android.util.math.ExpressionToLatexConverter.Companion.convertToLatex import org.oppia.android.util.math.NumericExpressionEvaluator.Companion.evaluate @@ -11,3 +20,29 @@ fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(div fun MathExpression.toRawLatex(divAsFraction: Boolean): String = convertToLatex(divAsFraction) fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() + +fun MathExpression.toComparableOperationList(): ComparableOperationList = + stripGroups().toComparable() + +private fun MathExpression.stripGroups(): MathExpression { + return when (expressionTypeCase) { + BINARY_OPERATION -> toBuilder().apply { + binaryOperation = binaryOperation.toBuilder().apply { + leftOperand = binaryOperation.leftOperand.stripGroups() + rightOperand = binaryOperation.rightOperand.stripGroups() + }.build() + }.build() + UNARY_OPERATION -> toBuilder().apply { + unaryOperation = unaryOperation.toBuilder().apply { + operand = unaryOperation.operand.stripGroups() + }.build() + }.build() + FUNCTION_CALL -> toBuilder().apply { + functionCall = functionCall.toBuilder().apply { + argument = functionCall.argument.stripGroups() + }.build() + }.build() + GROUP -> group.stripGroups() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> this + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 83605b05808..cbe17f0dc92 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -8,6 +8,8 @@ import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET import kotlin.math.pow +val REAL_COMPARATOR: Comparator by lazy { Comparator.comparing(Real::toDouble) } + fun Real.isRational(): Boolean = realTypeCase == RATIONAL fun Real.isInteger(): Boolean = realTypeCase == INTEGER 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 index 69d9dab509a..fcce3805364 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -42,6 +42,25 @@ oppia_android_test( ], ) +oppia_android_test( + name = "ExpressionToComparableOperationListTest", + srcs = ["ExpressionToComparableOperationListTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.ExpressionToComparableOperationListTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:comparable_operation_list_subject", + "//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", + "//utility/src/main/java/org/oppia/android/util/math:parser", + ], +) + oppia_android_test( name = "ExpressionToLatexTest", srcs = ["ExpressionToLatexTest.kt"], diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListTest.kt new file mode 100644 index 00000000000..d9599c0b32d --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListTest.kt @@ -0,0 +1,1637 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.PRODUCT +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.SUMMATION +import org.oppia.android.app.model.MathExpression +import org.oppia.android.testing.math.ComparableOperationListSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.robolectric.annotation.LooperMode + +/** Tests for [MathExpressionParser]. */ +// SameParameterValue: tests should have specific context included/excluded for readability. +@Suppress("SameParameterValue") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class ExpressionToComparableOperationListTest { + // TODO: add high-level checks for the three types, but don't test in detail since there are + // separate suites. Also, document the separate suites' existence in this suites's KDoc. + + @Test + fun testToComparableOperation() { + // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). + + val exp1 = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp1.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + + val exp2 = parseNumericExpressionSuccessfullyWithAllErrors("-1") + assertThat(exp2.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + + val exp3 = parseNumericExpressionSuccessfullyWithAllErrors("1+3+4") + assertThat(exp3.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp4 = parseNumericExpressionSuccessfullyWithAllErrors("-1-2-3") + assertThat(exp4.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp5 = parseNumericExpressionSuccessfullyWithAllErrors("1+2-3") + assertThat(exp5.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp6 = parseNumericExpressionSuccessfullyWithAllErrors("2*3*4") + assertThat(exp6.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp7 = parseNumericExpressionSuccessfullyWithAllErrors("1-2*3") + assertThat(exp7.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + + val exp8 = parseNumericExpressionSuccessfullyWithAllErrors("2*3-4") + assertThat(exp8.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp9 = parseNumericExpressionSuccessfullyWithAllErrors("1+2*3-4+8*7*6-9") + assertThat(exp9.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(8) + } + } + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(3) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(4) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(9) + } + } + } + } + + val exp10 = parseNumericExpressionSuccessfullyWithAllErrors("2/3/4") + assertThat(exp10.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp11 = parseNumericExpressionSuccessfullyWithoutOptionalErrors("2^3^4") + assertThat(exp11.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + } + + val exp12 = parseNumericExpressionSuccessfullyWithAllErrors("1+2/3+3") + assertThat(exp12.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp13 = parseNumericExpressionSuccessfullyWithAllErrors("1+(2/3)+3") + assertThat(exp13.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp14 = parseNumericExpressionSuccessfullyWithAllErrors("1+2^3+3") + assertThat(exp14.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp15 = parseNumericExpressionSuccessfullyWithAllErrors("1+(2^3)+3") + assertThat(exp15.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp16 = parseNumericExpressionSuccessfullyWithAllErrors("2*3/4*7") + assertThat(exp16.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(4) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp17 = parseNumericExpressionSuccessfullyWithAllErrors("2*(3/4)*7") + assertThat(exp17.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(4) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp18 = parseNumericExpressionSuccessfullyWithAllErrors("-3*sqrt(2)") + assertThat(exp18.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + squareRootWithArgument { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp19 = parseNumericExpressionSuccessfullyWithAllErrors("1+(2+(3+(4+5)))") + assertThat(exp19.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(4) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + + val exp20 = parseNumericExpressionSuccessfullyWithAllErrors("2*(3*(4*(5*6)))") + assertThat(exp20.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + index(4) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + } + } + + val exp21 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp21.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + + val exp22 = parseAlgebraicExpressionSuccessfullyWithAllErrors("-x") + assertThat(exp22.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + + val exp23 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x+y") + assertThat(exp23.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp24 = parseAlgebraicExpressionSuccessfullyWithAllErrors("-1-x-y") + assertThat(exp24.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp25 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x-y") + assertThat(exp25.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp26 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2xy") + assertThat(exp26.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp27 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1-xy") + assertThat(exp27.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + + val exp28 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy-4") + assertThat(exp28.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp29 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+xy-4+yz-9") + assertThat(exp29.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(3) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(4) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(9) + } + } + } + } + + val exp30 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2/x/y") + assertThat(exp30.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp31 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("x^3^4") + assertThat(exp31.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + } + + val exp32 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x/y+z") + assertThat(exp32.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + + val exp33 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x/y)+z") + assertThat(exp33.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + + val exp34 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x^3+y") + assertThat(exp34.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp35 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x^3)+y") + assertThat(exp35.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp36 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*x/y*z") + assertThat(exp36.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(4) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp37 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(x/y)*z") + assertThat(exp37.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(4) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp38 = parseAlgebraicExpressionSuccessfullyWithAllErrors("-2*sqrt(x)") + assertThat(exp38.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + squareRootWithArgument { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + + val exp39 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x+(3+(z+y)))") + assertThat(exp39.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + index(4) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + + val exp40 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(x*(4*(zy)))") + assertThat(exp40.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + index(4) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + + // Equality tests: + val list1 = createComparableOperationListFromNumericExpression("(1+2)+3") + val list2 = createComparableOperationListFromNumericExpression("1+(2+3)") + assertThat(list1).isEqualTo(list2) + + val list3 = createComparableOperationListFromNumericExpression("1+2+3") + val list4 = createComparableOperationListFromNumericExpression("3+2+1") + assertThat(list3).isEqualTo(list4) + + val list5 = createComparableOperationListFromNumericExpression("1-2-3") + val list6 = createComparableOperationListFromNumericExpression("-3 + -2 + 1") + assertThat(list5).isEqualTo(list6) + + val list7 = createComparableOperationListFromNumericExpression("1-2-3") + val list8 = createComparableOperationListFromNumericExpression("-3-2+1") + assertThat(list7).isEqualTo(list8) + + val list9 = createComparableOperationListFromNumericExpression("1-2-3") + val list10 = createComparableOperationListFromNumericExpression("-3-2+1") + assertThat(list9).isEqualTo(list10) + + val list11 = createComparableOperationListFromNumericExpression("1-2-3") + val list12 = createComparableOperationListFromNumericExpression("3-2-1") + assertThat(list11).isNotEqualTo(list12) + + val list13 = createComparableOperationListFromNumericExpression("2*3*4") + val list14 = createComparableOperationListFromNumericExpression("4*3*2") + assertThat(list13).isEqualTo(list14) + + val list15 = createComparableOperationListFromNumericExpression("2*(3/4)") + val list16 = createComparableOperationListFromNumericExpression("3/4*2") + assertThat(list15).isEqualTo(list16) + + val list17 = createComparableOperationListFromNumericExpression("2*3/4") + val list18 = createComparableOperationListFromNumericExpression("3/4*2") + assertThat(list17).isEqualTo(list18) + + val list45 = createComparableOperationListFromNumericExpression("2*3/4") + val list46 = createComparableOperationListFromNumericExpression("2*3*4") + assertThat(list45).isNotEqualTo(list46) + + val list19 = createComparableOperationListFromNumericExpression("2*3/4") + val list20 = createComparableOperationListFromNumericExpression("2*4/3") + assertThat(list19).isNotEqualTo(list20) + + val list21 = createComparableOperationListFromNumericExpression("2*3/4*7") + val list22 = createComparableOperationListFromNumericExpression("3/4*7*2") + assertThat(list21).isEqualTo(list22) + + val list23 = createComparableOperationListFromNumericExpression("2*3/4*7") + val list24 = createComparableOperationListFromNumericExpression("7*(3*2/4)") + assertThat(list23).isEqualTo(list24) + + val list25 = createComparableOperationListFromNumericExpression("2*3/4*7") + val list26 = createComparableOperationListFromNumericExpression("7*3*2/4") + assertThat(list25).isEqualTo(list26) + + val list27 = createComparableOperationListFromNumericExpression("-2*3") + val list28 = createComparableOperationListFromNumericExpression("3*-2") + assertThat(list27).isEqualTo(list28) + + val list29 = createComparableOperationListFromNumericExpression("2^3") + val list30 = createComparableOperationListFromNumericExpression("3^2") + assertThat(list29).isNotEqualTo(list30) + + val list31 = createComparableOperationListFromNumericExpression("-(1+2)") + val list32 = createComparableOperationListFromNumericExpression("-1+2") + assertThat(list31).isNotEqualTo(list32) + + val list33 = createComparableOperationListFromNumericExpression("-(1+2)") + val list34 = createComparableOperationListFromNumericExpression("-1-2") + assertThat(list33).isNotEqualTo(list34) + + val list35 = createComparableOperationListFromAlgebraicExpression("x(x+1)") + val list36 = createComparableOperationListFromAlgebraicExpression("(1+x)x") + assertThat(list35).isEqualTo(list36) + + val list37 = createComparableOperationListFromAlgebraicExpression("x(x+1)") + val list38 = createComparableOperationListFromAlgebraicExpression("x^2+x") + assertThat(list37).isNotEqualTo(list38) + + val list39 = createComparableOperationListFromAlgebraicExpression("x^2*sqrt(x)") + val list40 = createComparableOperationListFromAlgebraicExpression("x") + assertThat(list39).isNotEqualTo(list40) + + val list41 = createComparableOperationListFromAlgebraicExpression("xyz") + val list42 = createComparableOperationListFromAlgebraicExpression("zyx") + assertThat(list41).isEqualTo(list42) + + val list43 = createComparableOperationListFromAlgebraicExpression("1+xy-2") + val list44 = createComparableOperationListFromAlgebraicExpression("-2+1+yx") + assertThat(list43).isEqualTo(list44) + + // TODO: add tests for comparator/sorting & negation simplification? + } + + private fun createComparableOperationListFromNumericExpression(expression: String) = + parseNumericExpressionSuccessfullyWithAllErrors(expression).toComparableOperationList() + + private fun createComparableOperationListFromAlgebraicExpression(expression: String) = + parseAlgebraicExpressionSuccessfullyWithAllErrors(expression).toComparableOperationList() + + private companion object { + // TODO: fix helper API. + + private fun parseNumericExpressionSuccessfullyWithAllErrors( + expression: String + ): MathExpression { + val result = parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) + return (result as MathParsingResult.Success).result + } + + private fun parseNumericExpressionSuccessfullyWithoutOptionalErrors( + expression: String + ): MathExpression { + val result = + parseNumericExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY + ) + return (result as MathParsingResult.Success).result + } + + private fun parseNumericExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode + ): MathParsingResult { + return MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) + } + + private fun parseAlgebraicExpressionSuccessfullyWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables + ) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables, errorCheckingMode + ) + } + } +} From 917c9093c052df08eaa838e109c588c0d277f319 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 14:41:11 -0800 Subject: [PATCH 016/134] Add support for expression->polynomial conversion. This is mostly copied from #2173. --- .../org/oppia/android/util/math/BUILD.bazel | 14 + .../math/ExpressionToPolynomialConverter.kt | 110 ++ .../util/math/MathExpressionExtensions.kt | 4 + .../android/util/math/PolynomialExtensions.kt | 307 ++++++ .../oppia/android/util/math/RealExtensions.kt | 44 +- .../org/oppia/android/util/math/BUILD.bazel | 18 + .../util/math/ExpressionToPolynomialTest.kt | 945 ++++++++++++++++++ 7 files changed, 1438 insertions(+), 4 deletions(-) create mode 100644 utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt 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 index fbd04084430..3bdacb733c2 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -106,6 +106,7 @@ kt_android_library( deps = [ ":expression_to_comparable_operation_list_converter", ":expression_to_latex_converter", + ":expression_to_polynomial_converter", ":numeric_expression_evaluator", "//model/src/main/proto:math_java_proto_lite", ], @@ -117,6 +118,7 @@ kt_android_library( "PolynomialExtensions.kt", ], deps = [ + ":comparator_extensions", ":real_extensions", "//model/src/main/proto:math_java_proto_lite", ], @@ -168,6 +170,18 @@ kt_android_library( ], ) +kt_android_library( + name = "expression_to_polynomial_converter", + srcs = [ + "ExpressionToPolynomialConverter.kt", + ], + deps = [ + ":polynomial_extensions", + ":real_extensions", + "//model/src/main/proto:math_java_proto_lite", + ], +) + kt_android_library( name = "numeric_expression_evaluator", srcs = [ diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt new file mode 100644 index 00000000000..d16ac87fce1 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt @@ -0,0 +1,110 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathFunctionCall.FunctionType +import org.oppia.android.app.model.MathFunctionCall.FunctionType.FUNCTION_UNSPECIFIED +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import org.oppia.android.app.model.Polynomial +import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator + +class ExpressionToPolynomialConverter private constructor() { + companion object { + fun MathExpression.reduceToPolynomial(): Polynomial? = + replaceSquareRoots().reduceToPolynomialAux()?.removeUnnecessaryVariables()?.sort() + + private fun MathExpression.replaceSquareRoots(): MathExpression { + return when (expressionTypeCase) { + BINARY_OPERATION -> toBuilder().apply { + binaryOperation = binaryOperation.toBuilder().apply { + leftOperand = binaryOperation.leftOperand.replaceSquareRoots() + rightOperand = binaryOperation.rightOperand.replaceSquareRoots() + }.build() + }.build() + UNARY_OPERATION -> toBuilder().apply { + unaryOperation = unaryOperation.toBuilder().apply { + operand = unaryOperation.operand.replaceSquareRoots() + }.build() + }.build() + FUNCTION_CALL -> when (functionCall.functionType) { + SQUARE_ROOT -> toBuilder().apply { + // Replace the square root function call with the equivalent exponentiation. That is, + // sqrt(x)=x^(1/2). + binaryOperation = MathBinaryOperation.newBuilder().apply { + operator = EXPONENTIATE + leftOperand = functionCall.argument.replaceSquareRoots() + rightOperand = MathExpression.newBuilder().apply { + constant = ONE_HALF + }.build() + }.build() + }.build() + FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> this + } + GROUP -> group.replaceSquareRoots() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> this + } + } + + private fun MathExpression.reduceToPolynomialAux(): Polynomial? { + return when (expressionTypeCase) { + CONSTANT -> createConstantPolynomial(constant) + VARIABLE -> createSingleVariablePolynomial(variable) + BINARY_OPERATION -> binaryOperation.reduceToPolynomial() + UNARY_OPERATION -> unaryOperation.reduceToPolynomial() + // Both functions & groups should be removed ahead of polynomial reduction. + FUNCTION_CALL, GROUP, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathBinaryOperation.reduceToPolynomial(): Polynomial? { + val leftPolynomial = leftOperand.reduceToPolynomialAux() ?: return null + val rightPolynomial = rightOperand.reduceToPolynomialAux() ?: return null + return when (operator) { + ADD -> leftPolynomial + rightPolynomial + SUBTRACT -> leftPolynomial - rightPolynomial + MULTIPLY -> leftPolynomial * rightPolynomial + DIVIDE -> leftPolynomial / rightPolynomial + EXPONENTIATE -> leftPolynomial.pow(rightPolynomial) + BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> null + } + } + + private fun MathUnaryOperation.reduceToPolynomial(): Polynomial? { + return when (operator) { + NEGATE -> -(operand.reduceToPolynomialAux() ?: return null) + POSITIVE -> operand.reduceToPolynomialAux() // Positive unary changes nothing. + UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> null + } + } + + private fun createSingleVariablePolynomial(variableName: String): Polynomial { + return createSingleTermPolynomial( + Polynomial.Term.newBuilder().apply { + coefficient = ONE + addVariable( + Polynomial.Term.Variable.newBuilder().apply { + name = variableName + power = 1 + }.build() + ) + }.build() + ) + } + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 70fa465e30a..39e59ce99bb 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -10,9 +10,11 @@ import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CA import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.Polynomial import org.oppia.android.app.model.Real import org.oppia.android.util.math.ExpressionToComparableOperationListConverter.Companion.toComparable import org.oppia.android.util.math.ExpressionToLatexConverter.Companion.convertToLatex +import org.oppia.android.util.math.ExpressionToPolynomialConverter.Companion.reduceToPolynomial import org.oppia.android.util.math.NumericExpressionEvaluator.Companion.evaluate fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(divAsFraction) @@ -24,6 +26,8 @@ fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() fun MathExpression.toComparableOperationList(): ComparableOperationList = stripGroups().toComparable() +fun MathExpression.toPolynomial(): Polynomial? = stripGroups().reduceToPolynomial() + private fun MathExpression.stripGroups(): MathExpression { return when (expressionTypeCase) { BINARY_OPERATION -> toBuilder().apply { diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index a4ba72213be..74b1187b6e0 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -1,13 +1,43 @@ package org.oppia.android.util.math +import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.Polynomial import org.oppia.android.app.model.Polynomial.Term import org.oppia.android.app.model.Polynomial.Term.Variable import org.oppia.android.app.model.Real +import java.util.SortedSet + +private val POLYNOMIAL_VARIABLE_COMPARATOR: Comparator by lazy { + // Note that power is reversed because larger powers should actually be sorted ahead of smaller + // powers for the same variable name (but variable name still takes precedence). This ensures + // cases like x^2y+y^2x are sorted in that order. + Comparator.comparing(Variable::getName).thenComparingReversed(Variable::getPower) +} + +private val POLYNOMIAL_TERM_COMPARATOR: Comparator by lazy { + // First, sort by all variable names to ensure xy is placed ahead of xz. Then, sort by variable + // powers in order of the variables (such that x^2y is ranked higher thank xy). Finally, sort by + // the coefficient to ensure equality through the comparator works correctly (though in practice + // like terms should always be combined). Note the specific reversing happening here. It's done in + // this way so that sorted set bigger/smaller list is reversed (which matches expectations since + // larger terms should appear earlier in the results). This is implementing an ordering similar to + // https://en.wikipedia.org/wiki/Polynomial#Definition, except for multi-variable functions (where + // variables of higher degree are preferred over lower degree by lexicographical order of variable + // names). + Comparator.comparing>( + { term -> term.variableList.toSortedSet(POLYNOMIAL_VARIABLE_COMPARATOR) }, + POLYNOMIAL_VARIABLE_COMPARATOR.reversed().toSetComparator() + ).reversed().thenComparing(Term::getCoefficient, REAL_COMPARATOR) +} /** Returns whether this polynomial is a constant-only polynomial (contains no variables). */ fun Polynomial.isConstant(): Boolean = termCount == 1 && getTerm(0).variableCount == 0 +fun Polynomial.isSingleTerm(): Boolean = termList.size == 1 + +fun Polynomial.isApproximatelyZero(): Boolean = + termList.all { it.coefficient.isApproximatelyZero() } // Zero polynomials only have 0 coefs. + /** * Returns the first term coefficient from this polynomial. This corresponds to the whole value of * the polynomial iff isConstant() returns true, otherwise this value isn't useful. @@ -17,6 +47,10 @@ fun Polynomial.isConstant(): Boolean = termCount == 1 && getTerm(0).variableCoun */ fun Polynomial.getConstant(): Real = getTerm(0).coefficient +// Return the highest power to represent the degree of the polynomial. Reference: +// https://www.varsitytutors.com/algebra_1-help/how-to-find-the-degree-of-a-polynomial. +fun Polynomial.getDegree(): Int = getLeadingTerm().highestDegree() + fun Polynomial.toPlainText(): String { return termList.map { it.toPlainText() }.reduce { acc, termAnswerStr -> if (termAnswerStr.startsWith("-")) { @@ -49,3 +83,276 @@ private fun Term.toPlainText(): String { private fun Variable.toPlainText(): String { return if (power > 1) "$name^$power" else name } + +fun Polynomial.combineLikeTerms(): Polynomial { + // The following algorithm is expected to grow in space by O(N*M) and in time by O(N*m*log(m)) + // where N is the total number of terms, M is the total number of variables, and m is the largest + // single count of variables among all terms (this is assuming constant-time insertion for the + // underlying hashtable). + val newTerms = termList.groupBy { + it.variableList.sortedWith(POLYNOMIAL_VARIABLE_COMPARATOR) + }.mapValues { (_, coefficientTerms) -> + coefficientTerms.map { it.coefficient } + }.mapNotNull { (variables, coefficients) -> + // Combine like terms by summing their coefficients. + val newCoefficient = coefficients.reduce(Real::plus) + return@mapNotNull if (!newCoefficient.isApproximatelyZero()) { + Term.newBuilder().apply { + coefficient = newCoefficient + + // Remove variables with zero powers (since they evaluate to '1'). + addAllVariable(variables.filter { variable -> variable.power != 0 }) + }.build() + } else null // Zero terms should be removed. + } + return Polynomial.newBuilder().apply { + addAllTerm(newTerms) + }.build().ensureAtLeastConstant() +} + +fun Polynomial.removeUnnecessaryVariables(): Polynomial { + return Polynomial.newBuilder().apply { + addAllTerm( + this@removeUnnecessaryVariables.termList.filter { term -> + !term.coefficient.isApproximatelyZero() + } + ) + }.build().ensureAtLeastConstant() +} + +fun Polynomial.sort(): Polynomial = Polynomial.newBuilder().apply { + addAllTerm(this@sort.termList.sortedWith(POLYNOMIAL_TERM_COMPARATOR)) +}.build() + +operator fun Polynomial.unaryMinus(): Polynomial { + // Negating a polynomial just requires flipping the signs on all coefficients. + return toBuilder() + .clearTerm() + .addAllTerm(termList.map { it.toBuilder().setCoefficient(-it.coefficient).build() }) + .build() +} + +operator fun Polynomial.plus(rhs: Polynomial): Polynomial { + // Adding two polynomials just requires combining their terms lists (taking into account combining + // common terms). + return Polynomial.newBuilder().apply { + addAllTerm(this@plus.termList + rhs.termList) + }.build().combineLikeTerms().removeUnnecessaryVariables() +} + +operator fun Polynomial.minus(rhs: Polynomial): Polynomial { + // a - b = a + -b + return this + -rhs +} + +operator fun Polynomial.times(rhs: Polynomial): Polynomial { + // Polynomial multiplication is simply multiplying each term in one by each term in the other. + val crossMultipliedTerms = termList.flatMap { leftTerm -> + rhs.termList.map { rightTerm -> leftTerm * rightTerm } + } + + // Treat each multiplied term as a unique polynomial, then add them together (so that like terms + // can be properly combined). + return crossMultipliedTerms.map { createSingleTermPolynomial(it) }.reduce(Polynomial::plus) +} + +operator fun Polynomial.div(rhs: Polynomial): Polynomial? { + // See https://en.wikipedia.org/wiki/Polynomial_long_division#Pseudocode for reference. + if (rhs.isApproximatelyZero()) { + return null // Dividing by zero is invalid and thus cannot yield a polynomial. + } + + var quotient = createConstantPolynomial(ZERO) + var remainder = this + val leadingDivisorTerm = rhs.getLeadingTerm() + val divisorVariable = leadingDivisorTerm.highestDegreeVariable() + val divisorVariableName = divisorVariable?.name + val divisorDegree = leadingDivisorTerm.highestDegree() + while (!remainder.isApproximatelyZero() && remainder.getDegree() >= divisorDegree) { + // Attempt to divide the leading terms (this may fail). Note that the leading term should always + // be based on the divisor variable being used (otherwise subsequent division steps will be + // inconsistent and potentially fail to resolve). + val newTerm = + remainder.getLeadingTerm(matchedVariable = divisorVariableName) / leadingDivisorTerm + ?: return null + quotient += newTerm.toPolynomial() + remainder -= newTerm.toPolynomial() * rhs + } + return when { + remainder.isApproximatelyZero() -> quotient // Exact division (i.e. with no remainder). + remainder.isConstant() && rhs.isConstant() -> { + // Remainder is a constant term. + val remainingTerm = remainder.getConstant() / rhs.getConstant() + quotient + createConstantPolynomial(remainingTerm) + } + else -> null // Remainder is a polynomial, so the division failed. + } +} + +fun Polynomial.pow(exp: Polynomial): Polynomial? { + // Polynomial exponentiation is only supported if the right side is a constant polynomial, + // otherwise the result cannot be a polynomial (though could still be compared to another + // expression by utilizing sampling techniques). + return if (exp.isConstant()) pow(exp.getConstant()) else null +} + +fun createConstantPolynomial(constant: Real): Polynomial = + createSingleTermPolynomial(Term.newBuilder().setCoefficient(constant).build()) + +fun createSingleTermPolynomial(term: Term): Polynomial = + Polynomial.newBuilder().apply { addTerm(term) }.build() + +private fun Polynomial.pow(exp: Int): Polynomial { + // Anything raised to the power of 0 is 1. + if (exp == 0) return createConstantPolynomial(ONE) + if (exp == 1) return this + var newValue = this + for (i in 1 until exp) newValue *= this + return newValue +} + +private fun Polynomial.pow(rational: Fraction): Polynomial? { + // Polynomials with addition require factoring. + return if (isSingleTerm()) { + termList.first().pow(rational)?.toPolynomial() + } else null +} + +private fun Polynomial.pow(exp: Real): Polynomial? { + val shouldBeInverted = exp.isNegative() + val positivePower = if (shouldBeInverted) -exp else exp + val exponentiation = when { + // Constant polynomials can be raised by any constant. + isConstant() -> createConstantPolynomial(getConstant().pow(positivePower)) + + // Polynomials can only be raised to positive integers (or zero). + exp.isWholeNumber() -> exp.asWholeNumber()?.let { pow(it) } + + // Polynomials can potentially be raised by a fractional power. + exp.isRational() -> pow(exp.rational) + + // All other cases require factoring will definitely not compute to polynomials (such as + // irrational exponents). + else -> null + } + return if (shouldBeInverted) { + val onePolynomial = createConstantPolynomial(ONE) + // Note that this division is guaranteed to fail if the exponentiation result is a polynomial. + // Future implementations may leverage root-finding algorithms to factor for integer inverse + // powers (such as square root, cubic root, etc.). Non-integer inverse powers will require + // sampling. + exponentiation?.let { onePolynomial / it } + } else exponentiation +} + +private operator fun Term.times(rhs: Term): Term { + // The coefficients are always multiplied. + val combinedCoefficient = coefficient * rhs.coefficient + + // Next, create a combined list of new variables. + val combinedVariables = variableList + rhs.variableList + + // Simplify the variables by combining the exponents of like variables. Start with a map of 0 + // powers, then add in the powers of each variable and collect the final list of unique terms. + val variableNamesMap = mutableMapOf() + combinedVariables.forEach { + variableNamesMap.compute(it.name) { _, power -> + if (power != null) power + it.power else it.power + } + } + val newVariableList = variableNamesMap.map { (name, power) -> + Variable.newBuilder().setName(name).setPower(power).build() + } + + return Term.newBuilder() + .setCoefficient(combinedCoefficient) + .addAllVariable(newVariableList) + .build() +} + +private operator fun Term.div(rhs: Term): Term? { + val dividendPowerMap = variableList.toPowerMap() + val divisorPowerMap = rhs.variableList.toPowerMap() + + // If any variables are present in the divisor and not the dividend, this division won't work + // effectively. + if (!dividendPowerMap.keys.containsAll(divisorPowerMap.keys)) return null + + // Division is simply subtracting the powers of terms in the divisor from those in the dividend. + val quotientPowerMap = dividendPowerMap.mapValues { (name, power) -> + power - divisorPowerMap.getOrDefault(name, defaultValue = 0) + } + + // If there are any negative powers, the divisor can't effectively divide this value. + if (quotientPowerMap.values.any { it < 0 }) return null + + // Remove variables with powers of 0 since those have been fully divided. Also, divide the + // coefficients to finish the division. + return Term.newBuilder() + .setCoefficient(coefficient / rhs.coefficient) + .addAllVariable(quotientPowerMap.filter { (_, power) -> power > 0 }.toVariableList()) + .build() +} + +private fun Term.pow(rational: Fraction): Term? { + // Raising an exponent by an exponent just requires multiplying the two together. + val newVariablePowers = variableList.map { variable -> + variable.power.toWholeNumberFraction() * rational + } + + // If any powers are not whole numbers then the rational is likely representing a root and the + // term in question is not rootable to that degree. + if (newVariablePowers.any { !it.isOnlyWholeNumber() }) return null + + return Term.newBuilder().apply { + coefficient = this@pow.coefficient + addAllVariable( + this@pow.variableList.zip(newVariablePowers).map { (variable, newPower) -> + variable.toBuilder().apply { + power = newPower.toWholeNumber() + }.build() + } + ) + }.build() +} + +private fun Polynomial.ensureAtLeastConstant(): Polynomial { + return if (termCount == 0) { + Polynomial.newBuilder().apply { + addTerm( + Term.newBuilder().apply { + coefficient = ZERO + }.build() + ) + }.build() + } else this +} + +private fun Polynomial.getLeadingTerm(matchedVariable: String? = null): Term { + // Return the leading term. Reference: https://undergroundmathematics.org/glossary/leading-term. + return termList.filter { term -> + matchedVariable?.let { variableName -> + term.variableList.any { it.name == variableName } + } ?: true + }.reduce { maxTerm, term -> + val maxTermDegree = maxTerm.highestDegree() + val termDegree = term.highestDegree() + return@reduce if (termDegree > maxTermDegree) term else maxTerm + } +} + +private fun Term.highestDegreeVariable(): Variable? = variableList.maxByOrNull(Variable::getPower) + +private fun Term.highestDegree(): Int = highestDegreeVariable()?.power ?: 0 + +private fun Term.toPolynomial(): Polynomial { + return Polynomial.newBuilder().addTerm(this).build() +} + +private fun List.toPowerMap(): Map { + return associateBy({ it.name }, { it.power }) +} + +private fun Map.toVariableList(): List { + return map { (name, power) -> Variable.newBuilder().setName(name).setPower(power).build() } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index cbe17f0dc92..4359fee66aa 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -8,12 +8,37 @@ import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET import kotlin.math.pow +val ZERO: Real by lazy { + Real.newBuilder().apply { integer = 0 }.build() +} + +val ONE: Real by lazy { + Real.newBuilder().apply { integer = 1 }.build() +} + +val ONE_HALF: Real by lazy { + Real.newBuilder().apply { + rational = Fraction.newBuilder().apply { + numerator = 1 + denominator = 2 + }.build() + }.build() +} + val REAL_COMPARATOR: Comparator by lazy { Comparator.comparing(Real::toDouble) } fun Real.isRational(): Boolean = realTypeCase == RATIONAL fun Real.isInteger(): Boolean = realTypeCase == INTEGER +fun Real.isWholeNumber(): Boolean { + return when (realTypeCase) { + RATIONAL -> rational.isOnlyWholeNumber() + INTEGER -> true + IRRATIONAL, REALTYPE_NOT_SET, null -> false + } +} + fun Real.isNegative(): Boolean = when (realTypeCase) { RATIONAL -> rational.isNegative IRRATIONAL -> irrational < 0 @@ -21,6 +46,12 @@ fun Real.isNegative(): Boolean = when (realTypeCase) { REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") } +fun Real.isApproximatelyEqualTo(value: Double): Boolean { + return toDouble().approximatelyEquals(value) +} + +fun Real.isApproximatelyZero(): Boolean = isApproximatelyEqualTo(0.0) + fun Real.toDouble(): Double { return when (realTypeCase) { RATIONAL -> rational.toDouble() @@ -30,6 +61,15 @@ fun Real.toDouble(): Double { } } +fun Real.asWholeNumber(): Int? { + return when (realTypeCase) { + RATIONAL -> if (rational.isOnlyWholeNumber()) rational.toWholeNumber() else null + INTEGER -> integer + IRRATIONAL -> null + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + } +} + fun Real.toPlainText(): String = when (realTypeCase) { // Note that the rational part is first converted to an improper fraction since mixed fractions // can't be expressed as a single coefficient in typical polynomial syntax). @@ -39,10 +79,6 @@ fun Real.toPlainText(): String = when (realTypeCase) { REALTYPE_NOT_SET, null -> "" } -fun Real.isApproximatelyEqualTo(value: Double): Boolean { - return toDouble().approximatelyEquals(value) -} - operator fun Real.unaryMinus(): Real { return when (realTypeCase) { RATIONAL -> recompute { it.setRational(-rational) } 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 index fcce3805364..f5784442a09 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -81,6 +81,24 @@ oppia_android_test( ], ) +oppia_android_test( + name = "ExpressionToPolynomialTest", + srcs = ["ExpressionToPolynomialTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.ExpressionToPolynomialTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", + "//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:parser", + ], +) + oppia_android_test( name = "MathExpressionParserTest", srcs = ["MathExpressionParserTest.kt"], diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt new file mode 100644 index 00000000000..f8163f88c3c --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt @@ -0,0 +1,945 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.MathExpression +import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.robolectric.annotation.LooperMode + +/** Tests for [MathExpressionParser]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class ExpressionToPolynomialTest { + // TODO: add high-level checks for the three types, but don't test in detail since there are + // separate suites. Also, document the separate suites' existence in this suites's KDoc. + + @Test + fun testPolynomials() { + // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). + + val poly1 = parseNumericExpressionSuccessfully("1").toPolynomial() + assertThat(poly1).evaluatesToPlainTextThat().isEqualTo("1") + assertThat(poly1).isConstantThat().isIntegerThat().isEqualTo(1) + + val poly13 = parseNumericExpressionSuccessfully("1-1").toPolynomial() + assertThat(poly13).evaluatesToPlainTextThat().isEqualTo("0") + assertThat(poly13).isConstantThat().isIntegerThat().isEqualTo(0) + + val poly2 = parseNumericExpressionSuccessfully("3 + 4 * 2 / (1 - 5) ^ 2").toPolynomial() + assertThat(poly2).evaluatesToPlainTextThat().isEqualTo("7/2") + assertThat(poly2).isConstantThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(3) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + + val poly3 = + parseAlgebraicExpressionSuccessfullyWithAllErrors("133+3.14*x/(11-15)^2").toPolynomial() + assertThat(poly3).evaluatesToPlainTextThat().isEqualTo("0.19625x + 133") + assertThat(poly3).hasTermCountThat().isEqualTo(2) + assertThat(poly3).term(0).hasCoefficientThat().isIrrationalThat().isWithin(1e-5).of(0.19625) + assertThat(poly3).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly3).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly3).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly3).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(133) + assertThat(poly3).term(1).hasVariableCountThat().isEqualTo(0) + + val poly4 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2").toPolynomial() + assertThat(poly4).evaluatesToPlainTextThat().isEqualTo("x^2") + assertThat(poly4).hasTermCountThat().isEqualTo(1) + assertThat(poly4).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly4).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly4).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly4).term(0).variable(0).hasPowerThat().isEqualTo(2) + + val poly5 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy+x").toPolynomial() + assertThat(poly5).evaluatesToPlainTextThat().isEqualTo("xy + x") + assertThat(poly5).hasTermCountThat().isEqualTo(2) + assertThat(poly5).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly5).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly5).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly5).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly5).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly5).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly5).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly5).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly5).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly5).term(1).variable(0).hasPowerThat().isEqualTo(1) + + val poly6 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2x").toPolynomial() + assertThat(poly6).evaluatesToPlainTextThat().isEqualTo("2x") + assertThat(poly6).hasTermCountThat().isEqualTo(1) + assertThat(poly6).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly6).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(2) + assertThat(poly6).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly6).term(0).variable(0).hasPowerThat().isEqualTo(1) + + val poly30 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x+2").toPolynomial() + assertThat(poly30).evaluatesToPlainTextThat().isEqualTo("x + 2") + assertThat(poly30).hasTermCountThat().isEqualTo(2) + assertThat(poly30).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly30).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(0) + } + + val poly29 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2-3*x-10").toPolynomial() + assertThat(poly29).evaluatesToPlainTextThat().isEqualTo("x^2 - 3x - 10") + assertThat(poly29).hasTermCountThat().isEqualTo(3) + assertThat(poly29).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly29).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly29).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-10) + hasVariableCountThat().isEqualTo(0) + } + + val poly31 = parseAlgebraicExpressionSuccessfullyWithAllErrors("4*(x+2)").toPolynomial() + assertThat(poly31).evaluatesToPlainTextThat().isEqualTo("4x + 8") + assertThat(poly31).hasTermCountThat().isEqualTo(2) + assertThat(poly31).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(4) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly31).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(8) + hasVariableCountThat().isEqualTo(0) + } + + val poly7 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2xy^2z^3").toPolynomial() + assertThat(poly7).evaluatesToPlainTextThat().isEqualTo("2xy^2z^3") + assertThat(poly7).hasTermCountThat().isEqualTo(1) + assertThat(poly7).term(0).hasVariableCountThat().isEqualTo(3) + assertThat(poly7).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(2) + assertThat(poly7).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly7).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly7).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly7).term(0).variable(1).hasPowerThat().isEqualTo(2) + assertThat(poly7).term(0).variable(2).hasNameThat().isEqualTo("z") + assertThat(poly7).term(0).variable(2).hasPowerThat().isEqualTo(3) + + // Show that 7+xy+yz-3-xz-yz+3xy-4 combines into 4xy-xz (the eliminated terms should be gone). + val poly8 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy+yz-xz-yz+3xy").toPolynomial() + assertThat(poly8).evaluatesToPlainTextThat().isEqualTo("4xy - xz") + assertThat(poly8).hasTermCountThat().isEqualTo(2) + assertThat(poly8).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly8).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(4) + assertThat(poly8).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly8).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly8).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly8).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly8).term(1).hasVariableCountThat().isEqualTo(2) + assertThat(poly8).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(-1) + assertThat(poly8).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly8).term(1).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly8).term(1).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly8).term(1).variable(1).hasPowerThat().isEqualTo(1) + + // x+2x should become 3x since like terms are combined. + val poly9 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x+2x").toPolynomial() + assertThat(poly9).evaluatesToPlainTextThat().isEqualTo("3x") + assertThat(poly9).hasTermCountThat().isEqualTo(1) + assertThat(poly9).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly9).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(3) + assertThat(poly9).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly9).term(0).variable(0).hasPowerThat().isEqualTo(1) + + // xx^2 should become x^3 since like terms are combined. + val poly10 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xx^2").toPolynomial() + assertThat(poly10).evaluatesToPlainTextThat().isEqualTo("x^3") + assertThat(poly10).hasTermCountThat().isEqualTo(1) + assertThat(poly10).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly10).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly10).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly10).term(0).variable(0).hasPowerThat().isEqualTo(3) + + // No terms in this polynomial should be combined. + val poly11 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2+x+1").toPolynomial() + assertThat(poly11).evaluatesToPlainTextThat().isEqualTo("x^2 + x + 1") + assertThat(poly11).hasTermCountThat().isEqualTo(3) + assertThat(poly11).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly11).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly11).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly11).term(0).variable(0).hasPowerThat().isEqualTo(2) + assertThat(poly11).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly11).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly11).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly11).term(1).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly11).term(2).hasVariableCountThat().isEqualTo(0) + assertThat(poly11).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + + // No terms in this polynomial should be combined. + val poly12 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2 + x^2y").toPolynomial() + assertThat(poly12).evaluatesToPlainTextThat().isEqualTo("x^2y + x^2") + assertThat(poly12).hasTermCountThat().isEqualTo(2) + assertThat(poly12).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly12).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly12).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly12).term(0).variable(0).hasPowerThat().isEqualTo(2) + assertThat(poly12).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly12).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly12).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly12).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly12).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly12).term(1).variable(0).hasPowerThat().isEqualTo(2) + + // Ordering tests. Verify that ordering matches + // https://en.wikipedia.org/wiki/Polynomial#Definition (where multiple variables are sorted + // lexicographically). + + // The order of the terms in this polynomial should be reversed. + val poly14 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x+x^2+x^3").toPolynomial() + assertThat(poly14).evaluatesToPlainTextThat().isEqualTo("x^3 + x^2 + x + 1") + assertThat(poly14).hasTermCountThat().isEqualTo(4) + assertThat(poly14).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly14).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly14).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly14).term(0).variable(0).hasPowerThat().isEqualTo(3) + assertThat(poly14).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly14).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly14).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly14).term(1).variable(0).hasPowerThat().isEqualTo(2) + assertThat(poly14).term(2).hasVariableCountThat().isEqualTo(1) + assertThat(poly14).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly14).term(2).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly14).term(2).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly14).term(3).hasVariableCountThat().isEqualTo(0) + assertThat(poly14).term(3).hasCoefficientThat().isIntegerThat().isEqualTo(1) + + // The order of the terms in this polynomial should be preserved. + val poly15 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^3+x^2+x+1").toPolynomial() + assertThat(poly15).evaluatesToPlainTextThat().isEqualTo("x^3 + x^2 + x + 1") + assertThat(poly15).hasTermCountThat().isEqualTo(4) + assertThat(poly15).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly15).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly15).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly15).term(0).variable(0).hasPowerThat().isEqualTo(3) + assertThat(poly15).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly15).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly15).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly15).term(1).variable(0).hasPowerThat().isEqualTo(2) + assertThat(poly15).term(2).hasVariableCountThat().isEqualTo(1) + assertThat(poly15).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly15).term(2).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly15).term(2).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly15).term(3).hasVariableCountThat().isEqualTo(0) + assertThat(poly15).term(3).hasCoefficientThat().isIntegerThat().isEqualTo(1) + + // The order of the terms in this polynomial should be reversed. + val poly16 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy+xz+yz").toPolynomial() + assertThat(poly16).evaluatesToPlainTextThat().isEqualTo("xy + xz + yz") + assertThat(poly16).hasTermCountThat().isEqualTo(3) + assertThat(poly16).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly16).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly16).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly16).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly16).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(1).hasVariableCountThat().isEqualTo(2) + assertThat(poly16).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly16).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly16).term(1).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(1).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly16).term(1).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(2).hasVariableCountThat().isEqualTo(2) + assertThat(poly16).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly16).term(2).variable(0).hasNameThat().isEqualTo("y") + assertThat(poly16).term(2).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(2).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly16).term(2).variable(1).hasPowerThat().isEqualTo(1) + + // The order of the terms in this polynomial should be preserved. + val poly17 = parseAlgebraicExpressionSuccessfullyWithAllErrors("yz+xz+xy").toPolynomial() + assertThat(poly17).evaluatesToPlainTextThat().isEqualTo("xy + xz + yz") + assertThat(poly17).hasTermCountThat().isEqualTo(3) + assertThat(poly17).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly17).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly17).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly17).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly17).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(1).hasVariableCountThat().isEqualTo(2) + assertThat(poly17).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly17).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly17).term(1).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(1).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly17).term(1).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(2).hasVariableCountThat().isEqualTo(2) + assertThat(poly17).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly17).term(2).variable(0).hasNameThat().isEqualTo("y") + assertThat(poly17).term(2).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(2).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly17).term(2).variable(1).hasPowerThat().isEqualTo(1) + + val poly18 = + parseAlgebraicExpressionSuccessfullyWithAllErrors("3+x+y+xy+x^2y+xy^2+x^2y^2").toPolynomial() + assertThat(poly18).evaluatesToPlainTextThat().isEqualTo("x^2y^2 + x^2y + xy^2 + xy + x + y + 3") + assertThat(poly18).hasTermCountThat().isEqualTo(7) + assertThat(poly18).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly18).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly18).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly18).term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly18).term(4).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly18).term(5).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly18).term(6).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(0) + } + + // Ensure variables of coefficient and power of 0 are removed. + val poly22 = parseAlgebraicExpressionSuccessfullyWithAllErrors("0x").toPolynomial() + assertThat(poly22).evaluatesToPlainTextThat().isEqualTo("0") + assertThat(poly22).hasTermCountThat().isEqualTo(1) + assertThat(poly22).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(0) + hasVariableCountThat().isEqualTo(0) + } + + val poly23 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x-x").toPolynomial() + assertThat(poly23).evaluatesToPlainTextThat().isEqualTo("0") + assertThat(poly23).hasTermCountThat().isEqualTo(1) + assertThat(poly23).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(0) + hasVariableCountThat().isEqualTo(0) + } + + val poly24 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^0").toPolynomial() + assertThat(poly24).evaluatesToPlainTextThat().isEqualTo("1") + assertThat(poly24).hasTermCountThat().isEqualTo(1) + assertThat(poly24).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + + val poly25 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x/x").toPolynomial() + assertThat(poly25).evaluatesToPlainTextThat().isEqualTo("1") + assertThat(poly25).hasTermCountThat().isEqualTo(1) + assertThat(poly25).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + + val poly26 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^(2-2)").toPolynomial() + assertThat(poly26).evaluatesToPlainTextThat().isEqualTo("1") + assertThat(poly26).hasTermCountThat().isEqualTo(1) + assertThat(poly26).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + + val poly28 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x+1)/2").toPolynomial() + assertThat(poly28).evaluatesToPlainTextThat().isEqualTo("(1/2)x + 1/2") + assertThat(poly28).hasTermCountThat().isEqualTo(2) + assertThat(poly28).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly28).term(1).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(0) + } + + // Ensure like terms are combined after polynomial multiplication. + val poly20 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x-5)(x+2)").toPolynomial() + assertThat(poly20).evaluatesToPlainTextThat().isEqualTo("x^2 - 3x - 10") + assertThat(poly20).hasTermCountThat().isEqualTo(3) + assertThat(poly20).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly20).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly20).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-10) + hasVariableCountThat().isEqualTo(0) + } + + val poly21 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(1+x)^3").toPolynomial() + assertThat(poly21).evaluatesToPlainTextThat().isEqualTo("x^3 + 3x^2 + 3x + 1") + assertThat(poly21).hasTermCountThat().isEqualTo(4) + assertThat(poly21).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + assertThat(poly21).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly21).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly21).term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + + val poly27 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2*y^2 + 2").toPolynomial() + assertThat(poly27).evaluatesToPlainTextThat().isEqualTo("x^2y^2 + 2") + assertThat(poly27).hasTermCountThat().isEqualTo(2) + assertThat(poly27).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly27).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(0) + } + + val poly32 = + parseAlgebraicExpressionSuccessfullyWithAllErrors("(x^2-3*x-10)*(x+2)").toPolynomial() + assertThat(poly32).evaluatesToPlainTextThat().isEqualTo("x^3 - x^2 - 16x - 20") + assertThat(poly32).hasTermCountThat().isEqualTo(4) + assertThat(poly32).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + assertThat(poly32).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly32).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-16) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly32).term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-20) + hasVariableCountThat().isEqualTo(0) + } + + val poly33 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x-y)^3").toPolynomial() + assertThat(poly33).evaluatesToPlainTextThat().isEqualTo("x^3 - 3x^2y + 3xy^2 - y^3") + assertThat(poly33).hasTermCountThat().isEqualTo(4) + assertThat(poly33).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + assertThat(poly33).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly33).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly33).term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(3) + } + } + + // Ensure polynomial division works. + val poly19 = + parseAlgebraicExpressionSuccessfullyWithAllErrors("(x^2-3*x-10)/(x+2)").toPolynomial() + assertThat(poly19).evaluatesToPlainTextThat().isEqualTo("x - 5") + assertThat(poly19).hasTermCountThat().isEqualTo(2) + assertThat(poly19).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly19).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-5) + hasVariableCountThat().isEqualTo(0) + } + + val poly35 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(xy-5y)/y").toPolynomial() + assertThat(poly35).evaluatesToPlainTextThat().isEqualTo("x - 5") + assertThat(poly35).hasTermCountThat().isEqualTo(2) + assertThat(poly35).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly35).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-5) + hasVariableCountThat().isEqualTo(0) + } + + val poly36 = + parseAlgebraicExpressionSuccessfullyWithAllErrors("(x^2-2xy+y^2)/(x-y)").toPolynomial() + assertThat(poly36).evaluatesToPlainTextThat().isEqualTo("x - y") + assertThat(poly36).hasTermCountThat().isEqualTo(2) + assertThat(poly36).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly36).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + + // Example from https://www.kristakingmath.com/blog/predator-prey-systems-ghtcp-5e2r4-427ab. + val poly37 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x^3-y^3)/(x-y)").toPolynomial() + assertThat(poly37).evaluatesToPlainTextThat().isEqualTo("x^2 + xy + y^2") + assertThat(poly37).hasTermCountThat().isEqualTo(3) + assertThat(poly37).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly37).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly37).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + + // Multi-variable & more complex division. + val poly34 = + parseAlgebraicExpressionSuccessfullyWithAllErrors( + "(x^3-3x^2y+3xy^2-y^3)/(x-y)^2" + ).toPolynomial() + assertThat(poly34).evaluatesToPlainTextThat().isEqualTo("x - y") + assertThat(poly34).hasTermCountThat().isEqualTo(2) + assertThat(poly34).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly34).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + + val poly38 = parseNumericExpressionSuccessfully("2^-4").toPolynomial() + assertThat(poly38).evaluatesToPlainTextThat().isEqualTo("1/16") + assertThat(poly38).hasTermCountThat().isEqualTo(1) + assertThat(poly38).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(16) + } + hasVariableCountThat().isEqualTo(0) + } + + val poly39 = parseNumericExpressionSuccessfully("2^(3-6)").toPolynomial() + assertThat(poly39).evaluatesToPlainTextThat().isEqualTo("1/8") + assertThat(poly39).hasTermCountThat().isEqualTo(1) + assertThat(poly39).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(8) + } + hasVariableCountThat().isEqualTo(0) + } + + // x^-3 is not a valid polynomial (since polynomials can't have negative powers). + val poly40 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^(3-6)").toPolynomial() + assertThat(poly40).isNotValidPolynomial() + + // 2^x is not a polynomial. + val poly41 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("2^x").toPolynomial() + assertThat(poly41).isNotValidPolynomial() + + // 1/x is not a polynomial. + val poly42 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("1/x").toPolynomial() + assertThat(poly42).isNotValidPolynomial() + + val poly43 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x/2").toPolynomial() + assertThat(poly43).evaluatesToPlainTextThat().isEqualTo("(1/2)x") + assertThat(poly43).hasTermCountThat().isEqualTo(1) + assertThat(poly43).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + + val poly44 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x-3)/2").toPolynomial() + assertThat(poly44).evaluatesToPlainTextThat().isEqualTo("(1/2)x - 3/2") + assertThat(poly44).hasTermCountThat().isEqualTo(2) + assertThat(poly44).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly44).term(1).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isTrue() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(3) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(0) + } + + val poly45 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x-1)(x+1)").toPolynomial() + assertThat(poly45).evaluatesToPlainTextThat().isEqualTo("x^2 - 1") + assertThat(poly45).hasTermCountThat().isEqualTo(2) + assertThat(poly45).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly45).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(0) + } + + // √x is not a polynomial. + val poly46 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(x)").toPolynomial() + assertThat(poly46).isNotValidPolynomial() + + val poly47 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√(x^2)").toPolynomial() + assertThat(poly47).evaluatesToPlainTextThat().isEqualTo("x") + assertThat(poly47).hasTermCountThat().isEqualTo(1) + assertThat(poly47).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + + val poly51 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√(x^2y^2)").toPolynomial() + assertThat(poly51).evaluatesToPlainTextThat().isEqualTo("xy") + assertThat(poly51).hasTermCountThat().isEqualTo(1) + assertThat(poly51).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + + // A limitation in the current polynomial conversion is that sqrt(x) will fail due to it not + // have any polynomial representation. + val poly48 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√x^2").toPolynomial() + assertThat(poly48).isNotValidPolynomial() + + // √(x^2+2) may evaluate to a polynomial, but it requires factoring (which isn't yet supported). + val poly50 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√(x^2+2)").toPolynomial() + assertThat(poly50).isNotValidPolynomial() + + // Division by zero is undefined, so a polynomial can't be constructed. + val poly49 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("(x+2)/0").toPolynomial() + assertThat(poly49).isNotValidPolynomial() + + val poly52 = parsePolynomialFromNumericExpression("1") + val poly53 = parsePolynomialFromNumericExpression("0") + assertThat(poly52).isNotEqualTo(poly53) + + val poly54 = parsePolynomialFromNumericExpression("1+2") + val poly55 = parsePolynomialFromNumericExpression("3") + assertThat(poly54).isEqualTo(poly55) + + val poly56 = parsePolynomialFromNumericExpression("1-2") + val poly57 = parsePolynomialFromNumericExpression("-1") + assertThat(poly56).isEqualTo(poly57) + + val poly58 = parsePolynomialFromNumericExpression("2*3") + val poly59 = parsePolynomialFromNumericExpression("6") + assertThat(poly58).isEqualTo(poly59) + + val poly60 = parsePolynomialFromNumericExpression("2^3") + val poly61 = parsePolynomialFromNumericExpression("8") + assertThat(poly60).isEqualTo(poly61) + + val poly62 = parsePolynomialFromAlgebraicExpression("1+x") + val poly63 = parsePolynomialFromAlgebraicExpression("x+1") + assertThat(poly62).isEqualTo(poly63) + + val poly64 = parsePolynomialFromAlgebraicExpression("y+x") + val poly65 = parsePolynomialFromAlgebraicExpression("x+y") + assertThat(poly64).isEqualTo(poly65) + + val poly66 = parsePolynomialFromAlgebraicExpression("(x+1)^2") + val poly67 = parsePolynomialFromAlgebraicExpression("x^2+2x+1") + assertThat(poly66).isEqualTo(poly67) + + val poly68 = parsePolynomialFromAlgebraicExpression("(x+1)/2") + val poly69 = parsePolynomialFromAlgebraicExpression("x/2+(1/2)") + assertThat(poly68).isEqualTo(poly69) + + val poly70 = parsePolynomialFromAlgebraicExpression("x*2") + val poly71 = parsePolynomialFromAlgebraicExpression("2x") + assertThat(poly70).isEqualTo(poly71) + + val poly72 = parsePolynomialFromAlgebraicExpression("x(x+1)") + val poly73 = parsePolynomialFromAlgebraicExpression("x^2+x") + assertThat(poly72).isEqualTo(poly73) + } + + private fun parsePolynomialFromNumericExpression(expression: String) = + parseNumericExpressionSuccessfully(expression).toPolynomial() + + private fun parsePolynomialFromAlgebraicExpression(expression: String) = + parseAlgebraicExpressionSuccessfullyWithAllErrors(expression).toPolynomial() + + private companion object { + // TODO: fix helper API. + + private fun parseNumericExpressionSuccessfully(expression: String): MathExpression { + val result = parseNumericExpressionWithAllErrors(expression) + return (result as MathParsingResult.Success).result + } + + private fun parseNumericExpressionWithAllErrors( + expression: String + ): MathParsingResult { + return MathExpressionParser.parseNumericExpression(expression, ErrorCheckingMode.ALL_ERRORS) + } + + private fun parseAlgebraicExpressionSuccessfullyWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables + ) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables, errorCheckingMode + ) + } + } +} From 961b3d05c7c3af64c5fae3a08e9611b976ee8ae0 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 15:25:05 -0800 Subject: [PATCH 017/134] Add NumericExpressionInput classifiers. This doesn't hook them up to the application or tests yet (that will happen in a later PR). --- domain/BUILD.bazel | 1 + ...nteractionObjectTypeExtractorRepository.kt | 1 + ...putIsEquivalentToRuleClassifierProvider.kt | 58 +++++++++++++++++++ ...atchesExactlyWithRuleClassifierProvider.kt | 49 ++++++++++++++++ ...vialManipulationsRuleClassifierProvider.kt | 51 ++++++++++++++++ .../NumericExpressionInputModule.kt | 36 ++++++++++++ .../util/InteractionObjectExtensions.kt | 2 + model/src/main/proto/interaction_object.proto | 1 + 8 files changed, 199 insertions(+) create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index cf4b8098c8e..fed48085058 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -125,6 +125,7 @@ kt_android_library( "//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/math:parser", "//utility/src/main/java/org/oppia/android/util/networking:network_connection_util", "//utility/src/main/java/org/oppia/android/util/profile:directory_management_util", ], diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/InteractionObjectTypeExtractorRepository.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/InteractionObjectTypeExtractorRepository.kt index 9b72e5dea69..48bec5b8f5c 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/InteractionObjectTypeExtractorRepository.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/InteractionObjectTypeExtractorRepository.kt @@ -76,6 +76,7 @@ class InteractionObjectTypeExtractorRepository @Inject constructor() { createMapping(InteractionObject::getListOfSetsOfTranslatableHtmlContentIds) ObjectTypeCase.TRANSLATABLE_SET_OF_NORMALIZED_STRING -> createMapping(InteractionObject::getTranslatableSetOfNormalizedString) + ObjectTypeCase.MATH_EXPRESSION -> createMapping(InteractionObject::getMathExpression) ObjectTypeCase.OBJECTTYPE_NOT_SET -> createMapping { error("Invalid object type") } } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt new file mode 100644 index 00000000000..ecf89663802 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -0,0 +1,58 @@ +package org.oppia.android.domain.classify.rules.numericexpressioninput + +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.Polynomial +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.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.toPolynomial +import javax.inject.Inject + +class NumericExpressionInputIsEquivalentToRuleClassifierProvider @Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + writtenTranslationContext: WrittenTranslationContext + ): Boolean { + val answerExpression = parsePolynomial(answer) ?: return false + val inputExpression = parsePolynomial(input) ?: return false + return answerExpression == inputExpression + } + + private fun parsePolynomial(rawExpression: String): Polynomial? { + return when (val expResult = MathExpressionParser.parseNumericExpression(rawExpression)) { + is MathParsingResult.Success -> { + expResult.result.toPolynomial().also { + if (it == null) { + consoleLogger.w( + "NumericExpEquivalent", "Expression is not a supported polynomial: $rawExpression." + ) + } + } + } + is MathParsingResult.Failure -> { + consoleLogger.e( + "NumericExpEquivalent", + "Encountered expression that failed parsing. Expression: $rawExpression." + + " Failure: ${expResult.error}." + ) + null + } + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt new file mode 100644 index 00000000000..9ce52a59519 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -0,0 +1,49 @@ +package org.oppia.android.domain.classify.rules.numericexpressioninput + +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.MathExpression +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.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import javax.inject.Inject + +class NumericExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + writtenTranslationContext: WrittenTranslationContext + ): Boolean { + val answerExpression = parseNumericExpression(answer) ?: return false + val inputExpression = parseNumericExpression(input) ?: return false + return answerExpression == inputExpression + } + + private fun parseNumericExpression(rawExpression: String): MathExpression? { + return when (val expResult = MathExpressionParser.parseNumericExpression(rawExpression)) { + is MathParsingResult.Success -> expResult.result + is MathParsingResult.Failure -> { + consoleLogger.e( + "NumericExpMatchesExact", + "Encountered expression that failed parsing. Expression: $rawExpression." + + " Failure: ${expResult.error}." + ) + null + } + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt new file mode 100644 index 00000000000..d1ea260e948 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -0,0 +1,51 @@ +package org.oppia.android.domain.classify.rules.numericexpressioninput + +import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.InteractionObject +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.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.toComparableOperationList +import javax.inject.Inject + +class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider +@Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + writtenTranslationContext: WrittenTranslationContext + ): Boolean { + val answerExpression = parseComparableOperationList(answer) ?: return false + val inputExpression = parseComparableOperationList(input) ?: return false + return answerExpression == inputExpression + } + + private fun parseComparableOperationList(rawExpression: String): ComparableOperationList? { + return when (val expResult = MathExpressionParser.parseNumericExpression(rawExpression)) { + is MathParsingResult.Success -> expResult.result.toComparableOperationList() + is MathParsingResult.Failure -> { + consoleLogger.e( + "NumericExpTrivialManips", + "Encountered expression that failed parsing. Expression: $rawExpression." + + " Failure: ${expResult.error}." + ) + null + } + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt new file mode 100644 index 00000000000..cb010da48de --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt @@ -0,0 +1,36 @@ +package org.oppia.android.domain.classify.rules.numericexpressioninput + +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import dagger.multibindings.StringKey +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.FractionInputRules + +/** Module that binds rule classifiers corresponding to the numeric expression input interaction. */ +@Module +class NumericExpressionInputModule { + @Provides + @IntoMap + @StringKey("MatchesExactlyWith") + @FractionInputRules + internal fun provideNumericExpressionInputMatchesExactlyWithRuleClassifierProvider( + classifierProvider: NumericExpressionInputMatchesExactlyWithRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() + + @Provides + @IntoMap + @StringKey("MatchesUpToTrivialManipulations") + @FractionInputRules + internal fun provideNumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifier( + classifierProvider: NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() + + @Provides + @IntoMap + @StringKey("IsEquivalentTo") + @FractionInputRules + internal fun provideNumericExpressionInputIsEquivalentToRuleClassifier( + classifierProvider: NumericExpressionInputIsEquivalentToRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() +} 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 32f9123e852..8ad6c07ecd1 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 @@ -9,6 +9,7 @@ import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.FRACTION import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.IMAGE_WITH_REGIONS import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.LIST_OF_SETS_OF_HTML_STRING import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.LIST_OF_SETS_OF_TRANSLATABLE_HTML_CONTENT_IDS +import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.MATH_EXPRESSION import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.NON_NEGATIVE_INT import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.NORMALIZED_STRING import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.NUMBER_WITH_UNITS @@ -53,6 +54,7 @@ fun InteractionObject.toAnswerString(): String { LIST_OF_SETS_OF_TRANSLATABLE_HTML_CONTENT_IDS -> listOfSetsOfTranslatableHtmlContentIds.toAnswerString() TRANSLATABLE_SET_OF_NORMALIZED_STRING -> translatableSetOfNormalizedString.toAnswerString() + MATH_EXPRESSION -> mathExpression OBJECTTYPE_NOT_SET -> "" // The default InteractionObject should be an empty string. } } diff --git a/model/src/main/proto/interaction_object.proto b/model/src/main/proto/interaction_object.proto index bb6154f9255..1e7b95ed7ef 100644 --- a/model/src/main/proto/interaction_object.proto +++ b/model/src/main/proto/interaction_object.proto @@ -29,6 +29,7 @@ message InteractionObject { TranslatableHtmlContentId translatable_html_content_id = 14; SetOfTranslatableHtmlContentIds set_of_translatable_html_content_ids = 15; ListOfSetsOfTranslatableHtmlContentIds list_of_sets_of_translatable_html_content_ids = 16; + string math_expression = 17; } } From 535ae2a8108f49dd8cb68e2443550b21d9e4be1f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 17:35:27 -0800 Subject: [PATCH 018/134] Introduce ClassificationContext. This refactor introduces support for passing customization arguments down to classifiers (which is needed for algebraic expression input). --- .../AnswerClassificationController.kt | 7 ++- .../domain/classify/ClassificationContext.kt | 10 +++ .../android/domain/classify/RuleClassifier.kt | 3 +- .../classify/rules/GenericRuleClassifier.kt | 48 +++++++------- ...asElementXAtPositionYClassifierProvider.kt | 4 +- ...lementXBeforeElementYClassifierProvider.kt | 4 +- ...nputIsEqualToOrderingClassifierProvider.kt | 4 +- ...emAtIncorrectPositionClassifierProvider.kt | 4 +- ...enominatorEqualToRuleClassifierProvider.kt | 4 +- ...artExactlyEqualToRuleClassifierProvider.kt | 4 +- ...ntegerPartEqualToRuleClassifierProvider.kt | 4 +- ...sNoFractionalPartRuleClassifierProvider.kt | 4 +- ...sNumeratorEqualToRuleClassifierProvider.kt | 4 +- ...AndInSimplestFormRuleClassifierProvider.kt | 4 +- ...putIsEquivalentToRuleClassifierProvider.kt | 4 +- ...tIsExactlyEqualToRuleClassifierProvider.kt | 4 +- ...nputIsGreaterThanRuleClassifierProvider.kt | 4 +- ...onInputIsLessThanRuleClassifierProvider.kt | 4 +- ...ckInputIsInRegionRuleClassifierProvider.kt | 4 +- ...tainsAtLeastOneOfRuleClassifierProvider.kt | 4 +- ...ntainAtLeastOneOfRuleClassifierProvider.kt | 4 +- ...ectionInputEqualsRuleClassifierProvider.kt | 4 +- ...tIsProperSubsetOfRuleClassifierProvider.kt | 4 +- ...ChoiceInputEqualsRuleClassifierProvider.kt | 4 +- ...ithUnitsIsEqualToRuleClassifierProvider.kt | 4 +- ...itsIsEquivalentToRuleClassifierProvider.kt | 4 +- ...putIsEquivalentToRuleClassifierProvider.kt | 4 +- ...atchesExactlyWithRuleClassifierProvider.kt | 4 +- ...vialManipulationsRuleClassifierProvider.kt | 4 +- ...umericInputEqualsRuleClassifierProvider.kt | 4 +- ...aterThanOrEqualToRuleClassifierProvider.kt | 4 +- ...nputIsGreaterThanRuleClassifierProvider.kt | 4 +- ...nclusivelyBetweenRuleClassifierProvider.kt | 4 +- ...LessThanOrEqualToRuleClassifierProvider.kt | 4 +- ...icInputIsLessThanRuleClassifierProvider.kt | 4 +- ...IsWithinToleranceRuleClassifierProvider.kt | 4 +- .../RatioInputEqualsRuleClassifierProvider.kt | 4 +- ...sNumberOfTermsEqualToClassifierProvider.kt | 4 +- ...ecificTermEqualToRuleClassifierProvider.kt | 4 +- ...InputIsEquivalentRuleClassifierProvider.kt | 4 +- ...TextInputContainsRuleClassifierProvider.kt | 9 ++- .../TextInputEqualsRuleClassifierProvider.kt | 9 ++- ...tInputFuzzyEqualsRuleClassifierProvider.kt | 9 ++- ...xtInputStartsWithRuleClassifierProvider.kt | 9 ++- ...tXAtPositionYRuleClassifierProviderTest.kt | 22 +++---- ...eforeElementYRuleClassifierProviderTest.kt | 20 +++--- ...IsEqualToOrderingClassifierProviderTest.kt | 14 ++--- ...IncorrectPositionClassifierProviderTest.kt | 14 ++--- ...inatorEqualToRuleClassifierProviderTest.kt | 14 ++--- ...xactlyEqualToRuleClassifierProviderTest.kt | 22 +++---- ...erPartEqualToRuleClassifierProviderTest.kt | 44 ++++++------- ...ractionalPartRuleClassifierProviderTest.kt | 18 +++--- ...eratorEqualToRuleClassifierProviderTest.kt | 16 ++--- ...nSimplestFormRuleClassifierProviderTest.kt | 32 +++++----- ...sEquivalentToRuleClassifierProviderTest.kt | 26 ++++---- ...xactlyEqualToRuleClassifierProviderTest.kt | 26 ++++---- ...IsGreaterThanRuleClassifierProviderTest.kt | 54 ++++++++-------- ...putIsLessThanRuleClassifierProviderTest.kt | 54 ++++++++-------- ...putIsInRegionRuleClassifierProviderTest.kt | 12 ++-- ...sAtLeastOneOfRuleClassifierProviderTest.kt | 14 ++--- ...nAtLeastOneOfRuleClassifierProviderTest.kt | 24 +++---- ...onInputEqualsRuleClassifierProviderTest.kt | 20 +++--- ...roperSubsetOfRuleClassifierProviderTest.kt | 26 ++++---- ...ceInputEqualsRuleClassifierProviderTest.kt | 12 ++-- ...nitsIsEqualToRuleClassifierProviderTest.kt | 16 ++--- ...sEquivalentToRuleClassifierProviderTest.kt | 18 +++--- ...icInputEqualsRuleClassifierProviderTest.kt | 20 +++--- ...ThanOrEqualToRuleClassifierProviderTest.kt | 26 ++++---- ...IsGreaterThanRuleClassifierProviderTest.kt | 26 ++++---- ...sivelyBetweenRuleClassifierProviderTest.kt | 38 ++++++------ ...ThanOrEqualToRuleClassifierProviderTest.kt | 30 ++++----- ...putIsLessThanRuleClassifierProviderTest.kt | 30 ++++----- ...thinToleranceRuleClassifierProviderTest.kt | 62 +++++++++---------- ...ioInputEqualsRuleClassifierProviderTest.kt | 12 ++-- ...berOfTermsEqualToClassifierProviderTest.kt | 10 +-- ...icTermEqualToRuleClassifierProviderTest.kt | 32 +++++----- ...tIsEquivalentRuleClassifierProviderTest.kt | 18 +++--- ...InputContainsRuleClassifierProviderTest.kt | 48 +++++++------- ...xtInputEqualsRuleClassifierProviderTest.kt | 40 +++++++----- ...utFuzzyEqualsRuleClassifierProviderTest.kt | 58 ++++++++++------- ...putStartsWithRuleClassifierProviderTest.kt | 52 +++++++++------- 81 files changed, 658 insertions(+), 610 deletions(-) create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/ClassificationContext.kt diff --git a/domain/src/main/java/org/oppia/android/domain/classify/AnswerClassificationController.kt b/domain/src/main/java/org/oppia/android/domain/classify/AnswerClassificationController.kt index 7281ff50485..55eb6023584 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/AnswerClassificationController.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/AnswerClassificationController.kt @@ -36,13 +36,14 @@ class AnswerClassificationController @Inject constructor( "expected one of: ${interactionClassifiers.keys}" } // TODO(#207): Add support for additional classification types. + interaction.customizationArgsMap return classifyAnswer( answer, interaction.answerGroupsList, interaction.defaultOutcome, interactionClassifier, interaction.id, - writtenTranslationContext + ClassificationContext(writtenTranslationContext, interaction.customizationArgsMap) ) } @@ -54,7 +55,7 @@ class AnswerClassificationController @Inject constructor( defaultOutcome: Outcome, interactionClassifier: InteractionClassifier, interactionId: String, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): ClassificationResult { for (answerGroup in answerGroups) { for (ruleSpec in answerGroup.ruleSpecsList) { @@ -65,7 +66,7 @@ class AnswerClassificationController @Inject constructor( " has: ${interactionClassifier.getRuleTypes()}" } try { - if (ruleClassifier.matches(answer, ruleSpec.inputMap, writtenTranslationContext)) { + if (ruleClassifier.matches(answer, ruleSpec.inputMap, classificationContext)) { // Explicit classification matched. return if (!answerGroup.hasTaggedSkillMisconception()) { ClassificationResult.OutcomeOnly(answerGroup.outcome) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/ClassificationContext.kt b/domain/src/main/java/org/oppia/android/domain/classify/ClassificationContext.kt new file mode 100644 index 00000000000..01415330ce2 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/ClassificationContext.kt @@ -0,0 +1,10 @@ +package org.oppia.android.domain.classify + +import org.oppia.android.app.model.SchemaObject +import org.oppia.android.app.model.WrittenTranslationContext + +data class ClassificationContext( + val writtenTranslationContext: WrittenTranslationContext = + WrittenTranslationContext.getDefaultInstance(), + val customizationArgs: Map = mapOf() +) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/RuleClassifier.kt b/domain/src/main/java/org/oppia/android/domain/classify/RuleClassifier.kt index f4fba3fc301..973b97e4fb2 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/RuleClassifier.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/RuleClassifier.kt @@ -1,7 +1,6 @@ package org.oppia.android.domain.classify import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext /** An answer classifier for a specific interaction rule. */ interface RuleClassifier { @@ -12,6 +11,6 @@ interface RuleClassifier { fun matches( answer: InteractionObject, inputs: Map, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/GenericRuleClassifier.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/GenericRuleClassifier.kt index 95f81ee1e9b..ed6efda5b4a 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/GenericRuleClassifier.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/GenericRuleClassifier.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import javax.inject.Inject @@ -15,15 +15,15 @@ import javax.inject.Inject */ // TODO(#1580): Re-restrict access using Bazel visibilities class GenericRuleClassifier constructor( - val expectedAnswerObjectType: InteractionObject.ObjectTypeCase, - val orderedExpectedParameterTypes: LinkedHashMap< + private val expectedAnswerObjectType: InteractionObject.ObjectTypeCase, + private val orderedExpectedParameterTypes: LinkedHashMap< String, InteractionObject.ObjectTypeCase>, - val matcherDelegate: MatcherDelegate + private val matcherDelegate: MatcherDelegate ) : RuleClassifier { override fun matches( answer: InteractionObject, inputs: Map, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { check(answer.objectTypeCase == expectedAnswerObjectType) { "Expected answer to be of type ${expectedAnswerObjectType.name} " + @@ -34,7 +34,7 @@ class GenericRuleClassifier constructor( .map { (parameterName, expectedObjectType) -> retrieveInputObject(parameterName, expectedObjectType, inputs) } - return matcherDelegate.matches(answer, parameterInputs, writtenTranslationContext) + return matcherDelegate.matches(answer, parameterInputs, classificationContext) } private fun retrieveInputObject( @@ -58,7 +58,7 @@ class GenericRuleClassifier constructor( * Returns whether the validated and extracted answer matches the expectations per the * specification of this classifier. */ - fun matches(answer: T, writtenTranslationContext: WrittenTranslationContext): Boolean + fun matches(answer: T, classificationContext: ClassificationContext): Boolean } interface SingleInputMatcher { @@ -66,7 +66,7 @@ class GenericRuleClassifier constructor( * Returns whether the validated and extracted answer matches the single validated and extracted * input parameter per the specification of this classifier. */ - fun matches(answer: T, input: T, writtenTranslationContext: WrittenTranslationContext): Boolean + fun matches(answer: T, input: T, classificationContext: ClassificationContext): Boolean } interface MultiTypeSingleInputMatcher { @@ -74,11 +74,7 @@ class GenericRuleClassifier constructor( * Returns whether the validated and extracted answer matches the single validated and extracted * input parameter per the specification of this classifier. */ - fun matches( - answer: AT, - input: IT, - writtenTranslationContext: WrittenTranslationContext - ): Boolean + fun matches(answer: AT, input: IT, classificationContext: ClassificationContext): Boolean } interface MultiTypeDoubleInputMatcher { @@ -90,7 +86,7 @@ class GenericRuleClassifier constructor( answer: AT, firstInput: ITF, secondInput: ITS, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean } @@ -103,7 +99,7 @@ class GenericRuleClassifier constructor( answer: T, firstInput: T, secondInput: T, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean } @@ -112,7 +108,7 @@ class GenericRuleClassifier constructor( abstract fun matches( answer: InteractionObject, inputs: List, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean class NoInputMatcherDelegate( @@ -122,10 +118,10 @@ class GenericRuleClassifier constructor( override fun matches( answer: InteractionObject, inputs: List, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { check(inputs.isEmpty()) - return matcher.matches(extractObject(answer), writtenTranslationContext) + return matcher.matches(extractObject(answer), classificationContext) } } @@ -136,11 +132,11 @@ class GenericRuleClassifier constructor( override fun matches( answer: InteractionObject, inputs: List, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { check(inputs.size == 1) return matcher.matches( - extractObject(answer), extractObject(inputs.first()), writtenTranslationContext + extractObject(answer), extractObject(inputs.first()), classificationContext ) } } @@ -153,11 +149,11 @@ class GenericRuleClassifier constructor( override fun matches( answer: InteractionObject, inputs: List, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { check(inputs.size == 1) return matcher.matches( - extractAnswerObject(answer), extractInputObject(inputs.first()), writtenTranslationContext + extractAnswerObject(answer), extractInputObject(inputs.first()), classificationContext ) } } @@ -169,14 +165,14 @@ class GenericRuleClassifier constructor( override fun matches( answer: InteractionObject, inputs: List, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { check(inputs.size == 2) return matcher.matches( extractObject(answer), extractObject(inputs[0]), extractObject(inputs[1]), - writtenTranslationContext + classificationContext ) } } @@ -190,14 +186,14 @@ class GenericRuleClassifier constructor( override fun matches( answer: InteractionObject, inputs: List, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { check(inputs.size == 2) return matcher.matches( extractAnswerObject(answer), extractFirstParamObject(inputs[0]), extractSecondParamObject(inputs[1]), - writtenTranslationContext + classificationContext ) } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXAtPositionYClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXAtPositionYClassifierProvider.kt index 72affc7d851..b6388896d97 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXAtPositionYClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXAtPositionYClassifierProvider.kt @@ -5,7 +5,7 @@ import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.NON_NEGATIVE import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.TRANSLATABLE_HTML_CONTENT_ID import org.oppia.android.app.model.ListOfSetsOfTranslatableHtmlContentIds import org.oppia.android.app.model.TranslatableHtmlContentId -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -45,7 +45,7 @@ class DragDropSortInputHasElementXAtPositionYClassifierProvider @Inject construc answer: ListOfContentIdSets1, firstInput: ContentId1, secondInput: Int, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { // Note that the '1' returned here is to have consistency with the web platform: matched indexes // start at 1 rather than 0 to make the indexes more human friendly. diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXBeforeElementYClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXBeforeElementYClassifierProvider.kt index 41fa56e9bcd..2c79702d5c9 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXBeforeElementYClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXBeforeElementYClassifierProvider.kt @@ -4,7 +4,7 @@ import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.LIST_OF_SETS import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.TRANSLATABLE_HTML_CONTENT_ID import org.oppia.android.app.model.ListOfSetsOfTranslatableHtmlContentIds import org.oppia.android.app.model.TranslatableHtmlContentId -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -44,7 +44,7 @@ class DragDropSortInputHasElementXBeforeElementYClassifierProvider @Inject const answer: ListOfContentIdSets2, firstInput: ContentId2, secondInput: ContentId2, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val answerSets = answer.contentIdListsList.map { it.getContentIdSet() } return answerSets.indexOfFirst { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingClassifierProvider.kt index 18b8335d51c..60fdbd5d66d 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingClassifierProvider.kt @@ -3,7 +3,7 @@ package org.oppia.android.domain.classify.rules.dragAndDropSortInput import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.LIST_OF_SETS_OF_TRANSLATABLE_HTML_CONTENT_IDS import org.oppia.android.app.model.ListOfSetsOfTranslatableHtmlContentIds import org.oppia.android.app.model.SetOfTranslatableHtmlContentIds -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -34,7 +34,7 @@ class DragDropSortInputIsEqualToOrderingClassifierProvider @Inject constructor( override fun matches( answer: ListOfSetsOfTranslatableHtmlContentIds, input: ListOfSetsOfTranslatableHtmlContentIds, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = areListOfSetsOfHtmlStringsEqual(answer, input) /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifierProvider.kt index acf28424dfe..64dcae58082 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.dragAndDropSortInput import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.LIST_OF_SETS_OF_TRANSLATABLE_HTML_CONTENT_IDS import org.oppia.android.app.model.ListOfSetsOfTranslatableHtmlContentIds -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -33,7 +33,7 @@ class DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifier override fun matches( answer: ListOfSetsOfTranslatableHtmlContentIds, input: ListOfSetsOfTranslatableHtmlContentIds, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val answerStringSets = answer.contentIdListsList val inputStringSets = input.contentIdListsList diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProvider.kt index 7467770aaee..1331c2d000c 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,7 +31,7 @@ class FractionInputHasDenominatorEqualToRuleClassifierProvider @Inject construct override fun matches( answer: Fraction, input: Int, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.denominator == input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProvider.kt index 5daf835e119..e8e75bce46d 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,7 +31,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProvider override fun matches( answer: Fraction, input: Fraction, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.numerator == input.numerator && answer.denominator == input.denominator } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProvider.kt index cdf8425ec19..81b953e27ca 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,7 +31,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProvider @Inject construct override fun matches( answer: Fraction, input: Int, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.wholeNumber == input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProvider.kt index 30b63ee8020..d47a4ea43cd 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -28,7 +28,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProvider @Inject constructor override fun matches( answer: Fraction, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.numerator == 0 } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProvider.kt index ac3da4c4d15..fa56ec057b5 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,7 +31,7 @@ class FractionInputHasNumeratorEqualToRuleClassifierProvider @Inject constructor override fun matches( answer: Fraction, input: Int, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.numerator == input } 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 f9498f7d965..9cb2dca6aac 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 @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -34,7 +34,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider override fun matches( answer: Fraction, input: Fraction, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { 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 e2c42f7ec67..8d745d5e3df 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 @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -32,7 +32,7 @@ class FractionInputIsEquivalentToRuleClassifierProvider @Inject constructor( override fun matches( answer: Fraction, input: Fraction, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.toDouble().approximatelyEquals(input.toDouble()) } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProvider.kt index 628824681c2..eaf41d88008 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -30,7 +30,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProvider @Inject constructor( override fun matches( answer: Fraction, input: Fraction, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer == input } 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 89d83f1e3d6..05d4936965c 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 @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,7 +31,7 @@ class FractionInputIsGreaterThanRuleClassifierProvider @Inject constructor( override fun matches( answer: Fraction, input: Fraction, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { 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 02d4b9766c9..b4e0fde2e20 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 @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,7 +31,7 @@ class FractionInputIsLessThanRuleClassifierProvider @Inject constructor( override fun matches( answer: Fraction, input: Fraction, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.toDouble() < input.toDouble() } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/ImageClickInputIsInRegionRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/ImageClickInputIsInRegionRuleClassifierProvider.kt index 3504fdaa68e..4f13484722f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/ImageClickInputIsInRegionRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/ImageClickInputIsInRegionRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.imageClickInput import org.oppia.android.app.model.ClickOnImage import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,7 +31,7 @@ class ImageClickInputIsInRegionRuleClassifierProvider @Inject constructor( override fun matches( answer: ClickOnImage, input: String, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.clickedRegionsList.indexOf(input) != -1 } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProvider.kt index f434bdd263c..f84e30b9b32 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.itemselectioninput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.SetOfTranslatableHtmlContentIds -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -32,6 +32,6 @@ class ItemSelectionInputContainsAtLeastOneOfRuleClassifierProvider @Inject const override fun matches( answer: SetOfTranslatableHtmlContentIds, input: SetOfTranslatableHtmlContentIds, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer.getContentIdSet().intersect(input.getContentIdSet()).isNotEmpty() } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProvider.kt index 5dbe1330c19..e3d306c9afc 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.itemselectioninput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.SetOfTranslatableHtmlContentIds -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -33,6 +33,6 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProvider override fun matches( answer: SetOfTranslatableHtmlContentIds, input: SetOfTranslatableHtmlContentIds, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer.getContentIdSet().intersect(input.getContentIdSet()).isEmpty() } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProvider.kt index c447df24546..4227906dbaf 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.itemselectioninput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.SetOfTranslatableHtmlContentIds -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -32,6 +32,6 @@ class ItemSelectionInputEqualsRuleClassifierProvider @Inject constructor( override fun matches( answer: SetOfTranslatableHtmlContentIds, input: SetOfTranslatableHtmlContentIds, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer.getContentIdSet() == input.getContentIdSet() } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProvider.kt index ba14381872a..c7a35286733 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.itemselectioninput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.SetOfTranslatableHtmlContentIds -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -32,7 +32,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProvider @Inject construct override fun matches( answer: SetOfTranslatableHtmlContentIds, input: SetOfTranslatableHtmlContentIds, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val answerSet = answer.getContentIdSet() val inputSet = input.getContentIdSet() diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/MultipleChoiceInputEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/MultipleChoiceInputEqualsRuleClassifierProvider.kt index 8814f50fee4..09254a65821 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/MultipleChoiceInputEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/MultipleChoiceInputEqualsRuleClassifierProvider.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules.multiplechoiceinput import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -29,6 +29,6 @@ class MultipleChoiceInputEqualsRuleClassifierProvider @Inject constructor( override fun matches( answer: Int, input: Int, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer == input } 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 9a225cc41ca..004168c1d1c 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 @@ -3,7 +3,7 @@ package org.oppia.android.domain.classify.rules.numberwithunits import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.NumberWithUnits -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -33,7 +33,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProvider @Inject constructor( override fun matches( answer: NumberWithUnits, input: NumberWithUnits, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { // The number types must match. if (answer.numberTypeCase != input.numberTypeCase) { 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 e94fc9191e7..d3a8bfae04a 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 @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.numberwithunits import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.NumberWithUnits -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -33,7 +33,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProvider @Inject constructor( override fun matches( answer: NumberWithUnits, input: NumberWithUnits, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { // Units must match, but in different orders is fine. if (answer.unitList.toSet() != input.unitList.toSet()) { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt index ecf89663802..6af1e922742 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.numericexpressioninput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.Polynomial -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -27,7 +27,7 @@ class NumericExpressionInputIsEquivalentToRuleClassifierProvider @Inject constru override fun matches( answer: String, input: String, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val answerExpression = parsePolynomial(answer) ?: return false val inputExpression = parsePolynomial(input) ?: return false diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt index 9ce52a59519..4769660a921 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.numericexpressioninput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.MathExpression -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -26,7 +26,7 @@ class NumericExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject con override fun matches( answer: String, input: String, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val answerExpression = parseNumericExpression(answer) ?: return false val inputExpression = parseNumericExpression(input) ?: return false diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index d1ea260e948..60793efa071 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.numericexpressioninput import org.oppia.android.app.model.ComparableOperationList import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -28,7 +28,7 @@ class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvide override fun matches( answer: String, input: String, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val answerExpression = parseComparableOperationList(answer) ?: return false val inputExpression = parseComparableOperationList(input) ?: return false 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 2c7a6dc5212..7d0f45b793c 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 @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules.numericinput import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -29,6 +29,6 @@ class NumericInputEqualsRuleClassifierProvider @Inject constructor( override fun matches( answer: Double, input: Double, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = input.approximatelyEquals(answer) } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProvider.kt index 09021d836e7..dabdb9c762e 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProvider.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules.numericinput import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -28,6 +28,6 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProvider @Inject construct override fun matches( answer: Double, input: Double, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer >= input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProvider.kt index e62a916aa48..41b5532ca13 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProvider.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules.numericinput import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -28,6 +28,6 @@ class NumericInputIsGreaterThanRuleClassifierProvider @Inject constructor( override fun matches( answer: Double, input: Double, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer > input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProvider.kt index 052e5ebc60d..2c8e664402a 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProvider.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules.numericinput import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,6 +31,6 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProvider @Inject constructor answer: Double, firstInput: Double, secondInput: Double, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer in firstInput..secondInput } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProvider.kt index 034b8f61f40..3dc1ddce52c 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProvider.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules.numericinput import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -28,6 +28,6 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProvider @Inject constructor( override fun matches( answer: Double, input: Double, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer <= input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProvider.kt index 26d56bcaa86..3d5e9cf53ff 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProvider.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules.numericinput import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -28,6 +28,6 @@ class NumericInputIsLessThanRuleClassifierProvider @Inject constructor( override fun matches( answer: Double, input: Double, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer < input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProvider.kt index ab830d2e3ff..3b3980587a4 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProvider.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules.numericinput import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,6 +31,6 @@ class NumericInputIsWithinToleranceRuleClassifierProvider @Inject constructor( answer: Double, firstInput: Double, secondInput: Double, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer in (firstInput - secondInput)..(firstInput + secondInput) } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProvider.kt index c8fbe57a790..5165c781c02 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.ratioinput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.RatioExpression -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -28,6 +28,6 @@ class RatioInputEqualsRuleClassifierProvider @Inject constructor( override fun matches( answer: RatioExpression, input: RatioExpression, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer.ratioComponentList == input.ratioComponentList } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProvider.kt index 0506de932c3..bdf58bd8d1f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.ratioinput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.RatioExpression -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -29,6 +29,6 @@ class RatioInputHasNumberOfTermsEqualToClassifierProvider @Inject constructor( override fun matches( answer: RatioExpression, input: Int, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer.ratioComponentCount == input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasSpecificTermEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasSpecificTermEqualToRuleClassifierProvider.kt index 749e8bef6a4..75ecb50355f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasSpecificTermEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasSpecificTermEqualToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.ratioinput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.RatioExpression -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -33,6 +33,6 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProvider @Inject constructor answer: RatioExpression, firstInput: Int, secondInput: Int, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer.ratioComponentList.getOrNull(firstInput - 1) == secondInput } 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 f9b9b2e9df8..c21fcc0944c 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 @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.ratioinput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.RatioExpression -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -29,6 +29,6 @@ class RatioInputIsEquivalentRuleClassifierProvider @Inject constructor( override fun matches( answer: RatioExpression, input: RatioExpression, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer.toSimplestForm() == input.toSimplestForm() } 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..853b513da31 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 @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.textinput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.TranslatableSetOfNormalizedString -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -37,10 +37,13 @@ class TextInputContainsRuleClassifierProvider @Inject constructor( override fun matches( answer: String, input: TranslatableSetOfNormalizedString, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val normalizedAnswer = machineLocale.run { answer.normalizeWhitespace().toMachineLowerCase() } - val inputStringList = translationController.extractStringList(input, writtenTranslationContext) + val inputStringList = + translationController.extractStringList( + input, classificationContext.writtenTranslationContext + ) return inputStringList.any { normalizedAnswer.contains(machineLocale.run { it.normalizeWhitespace().toMachineLowerCase() }) } 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..a0c3034d6e7 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 @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.textinput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.TranslatableSetOfNormalizedString -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -37,10 +37,13 @@ class TextInputEqualsRuleClassifierProvider @Inject constructor( override fun matches( answer: String, input: TranslatableSetOfNormalizedString, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val normalizedAnswer = answer.normalizeWhitespace() - val inputStringList = translationController.extractStringList(input, writtenTranslationContext) + val inputStringList = + translationController.extractStringList( + input, classificationContext.writtenTranslationContext + ) return inputStringList.any { machineLocale.run { it.normalizeWhitespace().equalsIgnoreCase(normalizedAnswer) } } 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..c660a01f133 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 @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.textinput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.TranslatableSetOfNormalizedString -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -37,9 +37,12 @@ class TextInputFuzzyEqualsRuleClassifierProvider @Inject constructor( override fun matches( answer: String, input: TranslatableSetOfNormalizedString, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { - val inputStringList = translationController.extractStringList(input, writtenTranslationContext) + val inputStringList = + translationController.extractStringList( + input, classificationContext.writtenTranslationContext + ) return inputStringList.any { hasEditDistanceEqualToOne(it, answer) } } 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..39c6c7b83d5 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 @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.textinput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.TranslatableSetOfNormalizedString -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -37,10 +37,13 @@ class TextInputStartsWithRuleClassifierProvider @Inject constructor( override fun matches( answer: String, input: TranslatableSetOfNormalizedString, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val normalizedAnswer = machineLocale.run { answer.normalizeWhitespace().toMachineLowerCase() } - val inputStringList = translationController.extractStringList(input, writtenTranslationContext) + val inputStringList = + translationController.extractStringList( + input, classificationContext.writtenTranslationContext + ) return inputStringList.any { normalizedAnswer.startsWith( machineLocale.run { it.normalizeWhitespace().toMachineLowerCase() } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest.kt index d73f1a71934..53be477a285 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createListOfSetsOfTranslatableHtmlContentIds import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createNonNegativeInt import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createTranslatableHtmlContentId @@ -59,7 +59,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -79,7 +79,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -96,7 +96,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -113,7 +113,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -130,7 +130,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -147,7 +147,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -164,7 +164,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -178,7 +178,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -192,7 +192,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -206,7 +206,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest.kt index 06ea75a0cf3..c706db71f64 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createListOfSetsOfTranslatableHtmlContentIds import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createNonNegativeInt import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createTranslatableHtmlContentId @@ -60,7 +60,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -77,7 +77,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -94,7 +94,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -111,7 +111,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -128,7 +128,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -145,7 +145,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -162,7 +162,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -176,7 +176,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -190,7 +190,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingClassifierProviderTest.kt index f6efcddc2f6..2c65122884d 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createListOfSetsOfTranslatableHtmlContentIds import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -71,7 +71,7 @@ class DragDropSortInputIsEqualToOrderingClassifierProviderTest { isEqualToOrderingClassifierProvider.matches( answer = LIST_OF_SETS_12_3_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -85,7 +85,7 @@ class DragDropSortInputIsEqualToOrderingClassifierProviderTest { isEqualToOrderingClassifierProvider.matches( answer = LIST_OF_SETS_12_3_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -99,7 +99,7 @@ class DragDropSortInputIsEqualToOrderingClassifierProviderTest { isEqualToOrderingClassifierProvider.matches( answer = LIST_OF_SETS_12_3_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -113,7 +113,7 @@ class DragDropSortInputIsEqualToOrderingClassifierProviderTest { isEqualToOrderingClassifierProvider.matches( answer = LIST_OF_SETS_12_3_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -127,7 +127,7 @@ class DragDropSortInputIsEqualToOrderingClassifierProviderTest { isEqualToOrderingClassifierProvider.matches( answer = LIST_OF_SETS_12_3_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -141,7 +141,7 @@ class DragDropSortInputIsEqualToOrderingClassifierProviderTest { isEqualToOrderingClassifierProvider.matches( answer = LIST_OF_SETS_12_3_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifierProviderTest.kt index af473238da2..d62690db395 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createListOfSetsOfTranslatableHtmlContentIds import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createNonNegativeInt import org.oppia.android.domain.classify.RuleClassifier @@ -73,7 +73,7 @@ class DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifier isEqualToOrderingWithOneItemIncorrectClassifier.matches( answer = LIST_OF_SETS_12_4_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -92,7 +92,7 @@ class DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifier isEqualToOrderingWithOneItemIncorrectClassifier.matches( answer = LIST_OF_SETS_12_4_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -106,7 +106,7 @@ class DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifier isEqualToOrderingWithOneItemIncorrectClassifier.matches( answer = LIST_OF_SETS_1_4_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -120,7 +120,7 @@ class DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifier isEqualToOrderingWithOneItemIncorrectClassifier.matches( answer = LIST_OF_SETS_1_4_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -134,7 +134,7 @@ class DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifier isEqualToOrderingWithOneItemIncorrectClassifier.matches( answer = LIST_OF_SETS_1_4_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -148,7 +148,7 @@ class DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifier isEqualToOrderingWithOneItemIncorrectClassifier.matches( answer = LIST_OF_SETS_12_4_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProviderTest.kt index 3fa9315aaa0..a4641cc64ad 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -71,7 +71,7 @@ class FractionInputHasDenominatorEqualToRuleClassifierProviderTest { denominatorIsEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // This should match because whole numbers have a denominator of 1 by default @@ -86,7 +86,7 @@ class FractionInputHasDenominatorEqualToRuleClassifierProviderTest { denominatorIsEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -100,7 +100,7 @@ class FractionInputHasDenominatorEqualToRuleClassifierProviderTest { denominatorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -114,7 +114,7 @@ class FractionInputHasDenominatorEqualToRuleClassifierProviderTest { denominatorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -128,7 +128,7 @@ class FractionInputHasDenominatorEqualToRuleClassifierProviderTest { denominatorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -147,7 +147,7 @@ class FractionInputHasDenominatorEqualToRuleClassifierProviderTest { denominatorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest.kt index af55a78b926..56212d736d9 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -99,7 +99,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -113,7 +113,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -127,7 +127,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -141,7 +141,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_321, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // 123 and 321 match because they have the same fractional parts: 0/1. @@ -156,7 +156,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -170,7 +170,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -184,7 +184,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -198,7 +198,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -212,7 +212,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -231,7 +231,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProviderTest.kt index a9e2bf4eda8..1ce8cbc6e94 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -146,7 +146,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -159,7 +159,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_5_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -172,7 +172,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_5_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -185,7 +185,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_3_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -198,7 +198,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_3_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -211,7 +211,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -224,7 +224,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -237,7 +237,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_0_2_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -250,7 +250,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_1_2_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -263,7 +263,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_1_2_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -276,7 +276,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -289,7 +289,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_0_2_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -302,7 +302,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -315,7 +315,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_5_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -328,7 +328,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_5_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -341,7 +341,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_3_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -354,7 +354,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_3_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -366,7 +366,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_1_2_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -379,7 +379,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_1_2_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -394,7 +394,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { .matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -412,7 +412,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { .matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProviderTest.kt index 6d8a25460f8..cb72a6d9b50 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProviderTest.kt @@ -10,7 +10,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.robolectric.annotation.Config @@ -97,7 +97,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProviderTest { hasNoFractionalPartClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -111,7 +111,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProviderTest { hasNoFractionalPartClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -125,7 +125,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProviderTest { hasNoFractionalPartClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -139,7 +139,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProviderTest { hasNoFractionalPartClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_123_0_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -153,7 +153,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProviderTest { hasNoFractionalPartClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -167,7 +167,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProviderTest { hasNoFractionalPartClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_0_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -181,7 +181,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProviderTest { hasNoFractionalPartClassifierProvider.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -195,7 +195,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProviderTest { hasNoFractionalPartClassifierProvider.matches( answer = FRACTION_VALUE_TEST_20_OVER_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProviderTest.kt index 8b8352c78db..647d37cbdd3 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -79,7 +79,7 @@ class FractionInputHasNumeratorEqualToRuleClassifierProviderTest { numeratorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -93,7 +93,7 @@ class FractionInputHasNumeratorEqualToRuleClassifierProviderTest { numeratorIsEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -107,7 +107,7 @@ class FractionInputHasNumeratorEqualToRuleClassifierProviderTest { numeratorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -121,7 +121,7 @@ class FractionInputHasNumeratorEqualToRuleClassifierProviderTest { numeratorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -135,7 +135,7 @@ class FractionInputHasNumeratorEqualToRuleClassifierProviderTest { numeratorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -149,7 +149,7 @@ class FractionInputHasNumeratorEqualToRuleClassifierProviderTest { numeratorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -168,7 +168,7 @@ class FractionInputHasNumeratorEqualToRuleClassifierProviderTest { numeratorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest.kt index 5210a2162a1..1afbb516319 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -111,7 +111,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -126,7 +126,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -141,7 +141,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -156,7 +156,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -171,7 +171,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -186,7 +186,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // Even if creator does not input simplest form, learner's answer must still be in simplest form @@ -202,7 +202,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -217,7 +217,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -232,7 +232,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -247,7 +247,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -262,7 +262,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -277,7 +277,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -292,7 +292,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -310,7 +310,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -328,7 +328,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProviderTest.kt index 5453058e53b..a61d6a03b41 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -100,7 +100,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -114,7 +114,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -128,7 +128,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_2_OVER_8, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -142,7 +142,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_2_OVER_8, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -156,7 +156,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -170,7 +170,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_6_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -184,7 +184,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_55_1_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -198,7 +198,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_13_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -212,7 +212,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = WHOLE_NUMBER_VALUE_TEST_254, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -226,7 +226,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_55_1_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -240,7 +240,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_6_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -254,7 +254,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_2_OVER_8, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProviderTest.kt index 083294f9ce5..5280da48f8a 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -99,7 +99,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -113,7 +113,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_321, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -127,7 +127,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -141,7 +141,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -155,7 +155,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -169,7 +169,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -183,7 +183,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -197,7 +197,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -211,7 +211,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -225,7 +225,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -239,7 +239,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -258,7 +258,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProviderTest.kt index 63b132c8441..b14d9993e06 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -100,7 +100,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -113,7 +113,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -126,7 +126,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -139,7 +139,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -152,7 +152,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -165,7 +165,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -178,7 +178,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -191,7 +191,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -204,7 +204,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -217,7 +217,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -230,7 +230,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -243,7 +243,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -256,7 +256,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -269,7 +269,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -282,7 +282,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -295,7 +295,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -308,7 +308,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -321,7 +321,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -334,7 +334,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -347,7 +347,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -360,7 +360,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -373,7 +373,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -386,7 +386,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -399,7 +399,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -414,7 +414,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { .matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -432,7 +432,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { .matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProviderTest.kt index 8318da73310..1dda20b3d7c 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -100,7 +100,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -113,7 +113,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -126,7 +126,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -139,7 +139,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -152,7 +152,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -165,7 +165,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -178,7 +178,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -191,7 +191,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -204,7 +204,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -217,7 +217,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -230,7 +230,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -243,7 +243,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -256,7 +256,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -269,7 +269,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -282,7 +282,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -295,7 +295,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -308,7 +308,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -321,7 +321,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -334,7 +334,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -347,7 +347,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -360,7 +360,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -373,7 +373,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -386,7 +386,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -399,7 +399,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -414,7 +414,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { .matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -432,7 +432,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { .matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/imageClickInput/ImageClickInputIsInRegionRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/imageClickInput/ImageClickInputIsInRegionRuleClassifierProviderTest.kt index 7acae52a13b..0694388d0c9 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/imageClickInput/ImageClickInputIsInRegionRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/imageClickInput/ImageClickInputIsInRegionRuleClassifierProviderTest.kt @@ -12,7 +12,7 @@ import org.junit.runner.RunWith import org.oppia.android.app.model.ClickOnImage import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.Point2d -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -51,7 +51,7 @@ class ImageClickInputIsInRegionRuleClassifierProviderTest { isInRegionClassifierProvider.matches( answer = IMAGE_REGION_ABC_POSITION_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -65,7 +65,7 @@ class ImageClickInputIsInRegionRuleClassifierProviderTest { isInRegionClassifierProvider.matches( answer = IMAGE_REGION_ABC_POSITION_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -79,7 +79,7 @@ class ImageClickInputIsInRegionRuleClassifierProviderTest { isInRegionClassifierProvider.matches( answer = IMAGE_REGION_ABC_POSITION_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -93,7 +93,7 @@ class ImageClickInputIsInRegionRuleClassifierProviderTest { isInRegionClassifierProvider.matches( answer = IMAGE_REGION_ABC_POSITION_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -112,7 +112,7 @@ class ImageClickInputIsInRegionRuleClassifierProviderTest { isInRegionClassifierProvider.matches( answer = IMAGE_REGION_ABC_POSITION_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest.kt index e17ba337f4a..e182692d80b 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createSetOfTranslatableHtmlContentIds import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode @@ -52,7 +52,7 @@ class ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -65,7 +65,7 @@ class ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -78,7 +78,7 @@ class ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -91,7 +91,7 @@ class ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -104,7 +104,7 @@ class ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -117,7 +117,7 @@ class ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest.kt index f31abfa389c..99be995d0bc 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createInt import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createSetOfTranslatableHtmlContentIds import org.oppia.android.testing.assertThrows @@ -55,7 +55,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { val matches = inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -68,7 +68,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { val matches = inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -81,7 +81,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { val matches = inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -94,7 +94,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { val matches = inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -107,7 +107,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { val matches = inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -120,7 +120,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { val matches = inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -133,7 +133,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { val matches = inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_EMPTY, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -146,7 +146,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { val matches = inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -160,7 +160,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -177,7 +177,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -194,7 +194,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = DIFFERENT_INTERACTION_OBJECT_TYPE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProviderTest.kt index 748a43c7a12..927dcdc5f3f 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createSetOfTranslatableHtmlContentIds import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -54,7 +54,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -68,7 +68,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -82,7 +82,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -96,7 +96,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -110,7 +110,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -124,7 +124,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_MIXED_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -138,7 +138,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -152,7 +152,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -166,7 +166,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest.kt index 5e22f731b95..8997d85c434 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createSetOfTranslatableHtmlContentIds import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createString import org.oppia.android.testing.assertThrows @@ -55,7 +55,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -68,7 +68,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -81,7 +81,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -94,7 +94,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_126, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -107,7 +107,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_16, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -120,7 +120,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_NONE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -133,7 +133,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_NONE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -146,7 +146,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_6, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -159,7 +159,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -173,7 +173,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -190,7 +190,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_INVAILD, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -207,7 +207,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/MultipleChoiceInputEqualsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/MultipleChoiceInputEqualsRuleClassifierProviderTest.kt index 691b84e99a2..d193f4c795b 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/MultipleChoiceInputEqualsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/MultipleChoiceInputEqualsRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -49,7 +49,7 @@ class MultipleChoiceInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = NON_NEGATIVE_VALUE_TEST_0, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -62,7 +62,7 @@ class MultipleChoiceInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = NON_NEGATIVE_VALUE_TEST_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -76,7 +76,7 @@ class MultipleChoiceInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = NON_NEGATIVE_VALUE_TEST_0, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -93,7 +93,7 @@ class MultipleChoiceInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -110,7 +110,7 @@ class MultipleChoiceInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = NON_NEGATIVE_VALUE_TEST_0, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProviderTest.kt index 4557720a234..048e5574066 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -105,7 +105,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProviderTest { unitsIsEqualsRuleClassifier.matches( answer = ANSWER_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -119,7 +119,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProviderTest { unitsIsEqualsRuleClassifier.matches( answer = TEST_REAL_INPUT_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -133,7 +133,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProviderTest { unitsIsEqualsRuleClassifier.matches( answer = INPUT_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -147,7 +147,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProviderTest { unitsIsEqualsRuleClassifier.matches( answer = ANSWER_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -161,7 +161,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProviderTest { unitsIsEqualsRuleClassifier.matches( answer = ANSWER_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -175,7 +175,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProviderTest { unitsIsEqualsRuleClassifier.matches( answer = TEST_REAL_ANSWER_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -190,7 +190,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProviderTest { unitsIsEqualsRuleClassifier.matches( answer = DOUBLE_VALUE_TEST_DIFFERENT_TYPE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProviderTest.kt index 2176ab2837c..d53553b9d79 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -99,7 +99,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProviderTest { unitIsEquivalentRuleClassifier.matches( answer = DIFF_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -113,7 +113,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProviderTest { unitIsEquivalentRuleClassifier.matches( answer = ANSWER_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -127,7 +127,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProviderTest { unitIsEquivalentRuleClassifier.matches( answer = ANSWER_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -141,7 +141,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProviderTest { unitIsEquivalentRuleClassifier.matches( answer = DIFF_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -155,7 +155,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProviderTest { unitIsEquivalentRuleClassifier.matches( answer = ANSWER_TEST_REAL_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -169,7 +169,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProviderTest { unitIsEquivalentRuleClassifier.matches( answer = ANSWER_TEST_REAL_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -183,7 +183,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProviderTest { unitIsEquivalentRuleClassifier.matches( answer = DOUBLE_VALUE_TEST_DIFFERENT_TYPE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -202,7 +202,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProviderTest { unitIsEquivalentRuleClassifier.matches( answer = ANSWER_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } 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..b8f7e8a0f9f 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 @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.util.FLOAT_EQUALITY_INTERVAL import org.oppia.android.testing.assertThrows @@ -65,7 +65,7 @@ class NumericInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -79,7 +79,7 @@ class NumericInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -94,7 +94,7 @@ class NumericInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = FIVE_POINT_ONE_TIMES_FLOAT_EQUALITY_INTERVAL, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -108,7 +108,7 @@ class NumericInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -122,7 +122,7 @@ class NumericInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -136,7 +136,7 @@ class NumericInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -151,7 +151,7 @@ class NumericInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = SIX_TIMES_FLOAT_EQUALITY_INTERVAL, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -165,7 +165,7 @@ class NumericInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -182,7 +182,7 @@ class NumericInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest.kt index 53aacbb513a..b7298602e46 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -63,7 +63,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -77,7 +77,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -91,7 +91,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -105,7 +105,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -119,7 +119,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -133,7 +133,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -147,7 +147,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -161,7 +161,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -175,7 +175,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -189,7 +189,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -203,7 +203,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -220,7 +220,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProviderTest.kt index 26e38c83922..aa4f78a021d 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -64,7 +64,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { .matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -78,7 +78,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -92,7 +92,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -106,7 +106,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -120,7 +120,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -134,7 +134,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -148,7 +148,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -162,7 +162,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -176,7 +176,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -190,7 +190,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -204,7 +204,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -221,7 +221,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProviderTest.kt index fbe82358b66..504117c488e 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -75,7 +75,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -91,7 +91,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -107,7 +107,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -123,7 +123,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -139,7 +139,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -155,7 +155,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -171,7 +171,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -187,7 +187,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -203,7 +203,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -219,7 +219,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -235,7 +235,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -254,7 +254,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -273,7 +273,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -292,7 +292,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -311,7 +311,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -330,7 +330,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -349,7 +349,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -368,7 +368,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProviderTest.kt index c3894d36ccb..1bcc688f8c3 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -63,7 +63,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -77,7 +77,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -91,7 +91,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -105,7 +105,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -119,7 +119,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -133,7 +133,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -147,7 +147,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -161,7 +161,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -175,7 +175,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -189,7 +189,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -203,7 +203,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -220,7 +220,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -237,7 +237,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -254,7 +254,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProviderTest.kt index 50f1e00fbe7..97d152ce3e4 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -63,7 +63,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -77,7 +77,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -91,7 +91,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -105,7 +105,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -119,7 +119,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -133,7 +133,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -147,7 +147,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -161,7 +161,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -175,7 +175,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -189,7 +189,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -203,7 +203,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -220,7 +220,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -237,7 +237,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = POSITIVE_INT_VALUE_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -254,7 +254,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProviderTest.kt index b0a6148d256..d5481c0ac89 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -78,7 +78,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { answer = POSITIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -94,7 +94,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -110,7 +110,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -126,7 +126,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -142,7 +142,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -158,7 +158,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -174,7 +174,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -190,7 +190,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -206,7 +206,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -222,7 +222,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -238,7 +238,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -254,7 +254,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -270,7 +270,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -286,7 +286,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -302,7 +302,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -318,7 +318,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -334,7 +334,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -350,7 +350,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -366,7 +366,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -382,7 +382,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -397,7 +397,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -415,7 +415,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -434,7 +434,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -453,7 +453,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -472,7 +472,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -490,7 +490,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -508,7 +508,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -527,7 +527,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -546,7 +546,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -565,7 +565,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProviderTest.kt index 6e221e3a790..244cb981571 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -62,7 +62,7 @@ class RatioInputEqualsRuleClassifierProviderTest { equalsClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -76,7 +76,7 @@ class RatioInputEqualsRuleClassifierProviderTest { equalsClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -90,7 +90,7 @@ class RatioInputEqualsRuleClassifierProviderTest { equalsClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -104,7 +104,7 @@ class RatioInputEqualsRuleClassifierProviderTest { equalsClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -123,7 +123,7 @@ class RatioInputEqualsRuleClassifierProviderTest { equalsClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProviderTest.kt index 9a38307e9d5..2cbb3b324a4 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -58,7 +58,7 @@ class RatioInputHasNumberOfTermsEqualToClassifierProviderTest { hasNumberOfTermsEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -72,7 +72,7 @@ class RatioInputHasNumberOfTermsEqualToClassifierProviderTest { hasNumberOfTermsEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -86,7 +86,7 @@ class RatioInputHasNumberOfTermsEqualToClassifierProviderTest { hasNumberOfTermsEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -105,7 +105,7 @@ class RatioInputHasNumberOfTermsEqualToClassifierProviderTest { hasNumberOfTermsEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasSpecificTermEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasSpecificTermEqualToRuleClassifierProviderTest.kt index dbddb7e5483..81968647149 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasSpecificTermEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasSpecificTermEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -49,7 +49,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -66,7 +66,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // The ratio has value 3, but the value 2 was expected. @@ -84,7 +84,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // A value was expected at index 2, but the ratio doesn't have that. @@ -99,7 +99,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_3_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -113,7 +113,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_3_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // The ratio has value 3 at index 1, but the value 2 was expected. @@ -128,7 +128,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_3_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -142,7 +142,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_3_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // The ratio has value 2 at index 2, but the value 3 was expected. @@ -157,7 +157,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_3_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // A value was expected at index 3, but the ratio doesn't have that. @@ -172,7 +172,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // A value was expected at index 0, but the ratio doesn't have that. @@ -187,7 +187,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // A value was expected at index 4, but the ratio doesn't have that. @@ -202,7 +202,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -219,7 +219,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -236,7 +236,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -253,7 +253,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -268,7 +268,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = mapOf(), - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProviderTest.kt index 9b1e17c3c42..c93b44f8dc2 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -66,7 +66,7 @@ class RatioInputIsEquivalentRuleClassifierProviderTest { isEquivalentClassifierProvider.matches( answer = RATIO_VALUE_TEST_2_4_6, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -80,7 +80,7 @@ class RatioInputIsEquivalentRuleClassifierProviderTest { isEquivalentClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -94,7 +94,7 @@ class RatioInputIsEquivalentRuleClassifierProviderTest { isEquivalentClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -108,7 +108,7 @@ class RatioInputIsEquivalentRuleClassifierProviderTest { isEquivalentClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -122,7 +122,7 @@ class RatioInputIsEquivalentRuleClassifierProviderTest { isEquivalentClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -136,7 +136,7 @@ class RatioInputIsEquivalentRuleClassifierProviderTest { isEquivalentClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -150,7 +150,7 @@ class RatioInputIsEquivalentRuleClassifierProviderTest { isEquivalentClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -169,7 +169,7 @@ class RatioInputIsEquivalentRuleClassifierProviderTest { isEquivalentClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProviderTest.kt index 8c931bc5009..11c16049edc 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProviderTest.kt @@ -13,7 +13,7 @@ import dagger.Provides import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createNonNegativeInt import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createString import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createTranslatableSetOfNormalizedString @@ -95,7 +95,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -109,7 +109,7 @@ class TextInputContainsRuleClassifierProviderTest { inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_NULL, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -122,7 +122,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -135,7 +135,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_IS_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -148,7 +148,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_NOT_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -161,7 +161,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -174,7 +174,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -187,7 +187,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -200,7 +200,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -213,7 +213,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -227,7 +227,7 @@ class TextInputContainsRuleClassifierProviderTest { inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_NULL, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -240,7 +240,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -253,7 +253,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -267,7 +267,7 @@ class TextInputContainsRuleClassifierProviderTest { inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -284,7 +284,7 @@ class TextInputContainsRuleClassifierProviderTest { inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -302,7 +302,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = createString("an answer among many"), inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -315,7 +315,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = createString("uma resposta entre muitas"), inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // A Portuguese answer isn't reocgnized with this translation context. @@ -329,7 +329,9 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = createString("an answer among many"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // Even though the English string matches, the presence of the Portuguese context should trigger @@ -344,7 +346,9 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = createString("uma resposta entre muitas"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // The translation context provides a bridge between Portuguese & English. @@ -358,7 +362,9 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = createString("de outros"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // The Portuguese answer doesn't match. diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProviderTest.kt index 59dc509ca7f..6506df027fd 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProviderTest.kt @@ -13,7 +13,7 @@ import dagger.Provides import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createNonNegativeInt import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createString import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createTranslatableSetOfNormalizedString @@ -87,7 +87,7 @@ class TextInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -101,7 +101,7 @@ class TextInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -115,7 +115,7 @@ class TextInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -129,7 +129,7 @@ class TextInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_SINGLE_SPACES, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -142,7 +142,7 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_DIFFERENT_VALUE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -156,7 +156,7 @@ class TextInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_SINGLE_SPACES, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -169,7 +169,7 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = STRING_VALUE_THIS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -182,7 +182,7 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = STRING_VALUE_A_TEST, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -195,7 +195,7 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = STRING_VALUE_NOT_A_TEST, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -209,7 +209,7 @@ class TextInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -226,7 +226,7 @@ class TextInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -244,7 +244,7 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = createString("an answer"), inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -257,7 +257,7 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = createString("uma resposta"), inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // A Portuguese answer isn't reocgnized with this translation context. @@ -271,7 +271,9 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = createString("an answer"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // Even though the English string matches, the presence of the Portuguese context should trigger @@ -286,7 +288,9 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = createString("uma resposta"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // The translation context provides a bridge between Portuguese & English. @@ -300,7 +304,9 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = createString("uma resposta diferente"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // The Portuguese answer doesn't match. diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProviderTest.kt index 0642bc8879c..53617305bd3 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProviderTest.kt @@ -13,7 +13,7 @@ import dagger.Provides import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createNonNegativeInt import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createString import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createTranslatableSetOfNormalizedString @@ -89,7 +89,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -103,7 +103,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -117,7 +117,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -131,7 +131,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_DIFF_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -145,7 +145,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -159,7 +159,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -173,7 +173,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_DIFF_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -187,7 +187,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_DIFF_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -200,7 +200,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -213,7 +213,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_WITH_WHITESPACE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -226,7 +226,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_THIS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -239,7 +239,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -252,7 +252,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TESTING, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -266,7 +266,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -284,7 +284,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = createString("an answer"), inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -297,7 +297,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = createString("uma resposta"), inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // A Portuguese answer isn't reocgnized with this translation context. @@ -311,7 +311,9 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = createString("an answer"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // Even though the English string matches, the presence of the Portuguese context should trigger @@ -326,7 +328,9 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = createString("uma resposta"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // The translation context provides a bridge between Portuguese & English. @@ -340,7 +344,9 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = createString("uma reposta"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // A single misspelled letter should still result in a match in the same way as English. @@ -354,7 +360,9 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = createString("reposta"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // A missing word & a misspelled word should result in no match. @@ -368,7 +376,9 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = createString("إجاب"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "إجابة") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "إجابة") + ) ) // A single misspelled letter should still result in a match in the same way as English. @@ -382,7 +392,9 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = createString("إجا"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // Multiple missing letters should result in no match. diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProviderTest.kt index 1d0cd08ece0..1999f7ca984 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProviderTest.kt @@ -13,7 +13,7 @@ import dagger.Provides import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createNonNegativeInt import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createString import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createTranslatableSetOfNormalizedString @@ -93,7 +93,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -106,7 +106,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -119,7 +119,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -132,7 +132,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // The check should be case-insensitive. @@ -146,7 +146,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -159,7 +159,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // The check should be case-insensitive. @@ -173,7 +173,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -186,7 +186,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -199,7 +199,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -212,7 +212,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -225,7 +225,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_NULL, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -238,7 +238,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_NULL, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -251,7 +251,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_ANTIDERIVATIVE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -264,7 +264,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_PREFIX, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -277,7 +277,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_SOMETHING_ELSE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -291,7 +291,7 @@ class TextInputStartsWithRuleClassifierProviderTest { inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -308,7 +308,7 @@ class TextInputStartsWithRuleClassifierProviderTest { inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -326,7 +326,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = createString("an answer is my choice"), inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -339,7 +339,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = createString("uma resposta é minha escolha"), inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // A Portuguese answer isn't reocgnized with this translation context. @@ -353,7 +353,9 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = createString("an answer is my choice"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // Even though the English string matches, the presence of the Portuguese context should trigger @@ -368,7 +370,9 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = createString("uma resposta é minha escolha"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // The translation context provides a bridge between Portuguese & English. @@ -382,7 +386,9 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = createString("diferente"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // The Portuguese answer doesn't match. From fef2d976ec89ad02c198c7d25b743ba762424812 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 17:47:07 -0800 Subject: [PATCH 019/134] Add classifiers for AlgebraicExpressionInput. --- ...putIsEquivalentToRuleClassifierProvider.kt | 69 +++++++++++++++++++ ...atchesExactlyWithRuleClassifierProvider.kt | 62 +++++++++++++++++ ...vialManipulationsRuleClassifierProvider.kt | 64 +++++++++++++++++ .../AlgebraicExpressionInputModule.kt | 36 ++++++++++ 4 files changed, 231 insertions(+) create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt new file mode 100644 index 00000000000..899a14682d6 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -0,0 +1,69 @@ +package org.oppia.android.domain.classify.rules.algebraicexpressioninput + +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.Polynomial +import org.oppia.android.domain.classify.ClassificationContext +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.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.toPolynomial +import javax.inject.Inject +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression + +class AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider @Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + classificationContext: ClassificationContext + ): Boolean { + val allowedVariables = classificationContext.extractAllowedVariables() + val answerExpression = parsePolynomial(answer, allowedVariables) ?: return false + val inputExpression = parsePolynomial(input, allowedVariables) ?: return false + return answerExpression == inputExpression + } + + private fun parsePolynomial(rawExpression: String, allowedVariables: List): Polynomial? { + return when (val expResult = parseAlgebraicExpression(rawExpression, allowedVariables)) { + is MathParsingResult.Success -> { + expResult.result.toPolynomial().also { + if (it == null) { + consoleLogger.w( + "AlgebraExpEquivalent", "Expression is not a supported polynomial: $rawExpression." + ) + } + } + } + is MathParsingResult.Failure -> { + consoleLogger.e( + "AlgebraExpEquivalent", + "Encountered expression that failed parsing. Expression: $rawExpression." + + " Failure: ${expResult.error}." + ) + null + } + } + } + + private companion object { + private fun ClassificationContext.extractAllowedVariables(): List { + return customizationArgs["customOskLetters"] + ?.schemaObjectList + ?.schemaObjectList + ?.map { it.normalizedString } + ?: listOf() + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt new file mode 100644 index 00000000000..fb521383d4c --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -0,0 +1,62 @@ +package org.oppia.android.domain.classify.rules.algebraicexpressioninput + +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.MathExpression +import org.oppia.android.domain.classify.ClassificationContext +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.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import javax.inject.Inject +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression + +class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + classificationContext: ClassificationContext + ): Boolean { + val allowedVariables = classificationContext.extractAllowedVariables() + val answerExpression = parseExpression(answer, allowedVariables) ?: return false + val inputExpression = parseExpression(input, allowedVariables) ?: return false + return answerExpression == inputExpression + } + + private fun parseExpression( + rawExpression: String, allowedVariables: List + ): MathExpression? { + return when (val expResult = parseAlgebraicExpression(rawExpression, allowedVariables)) { + is MathParsingResult.Success -> expResult.result + is MathParsingResult.Failure -> { + consoleLogger.e( + "AlgebraExpMatchesExact", + "Encountered expression that failed parsing. Expression: $rawExpression." + + " Failure: ${expResult.error}." + ) + null + } + } + } + + private companion object { + private fun ClassificationContext.extractAllowedVariables(): List { + return customizationArgs["customOskLetters"] + ?.schemaObjectList + ?.schemaObjectList + ?.map { it.normalizedString } + ?: listOf() + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt new file mode 100644 index 00000000000..b56fe353d39 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -0,0 +1,64 @@ +package org.oppia.android.domain.classify.rules.algebraicexpressioninput + +import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.domain.classify.ClassificationContext +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.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.toComparableOperationList +import javax.inject.Inject +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression + +class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider +@Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + classificationContext: ClassificationContext + ): Boolean { + val allowedVariables = classificationContext.extractAllowedVariables() + val answerExpression = parseComparableOperationList(answer, allowedVariables) ?: return false + val inputExpression = parseComparableOperationList(input, allowedVariables) ?: return false + return answerExpression == inputExpression + } + + private fun parseComparableOperationList( + rawExpression: String, allowedVariables: List + ): ComparableOperationList? { + return when (val expResult = parseAlgebraicExpression(rawExpression, allowedVariables)) { + is MathParsingResult.Success -> expResult.result.toComparableOperationList() + is MathParsingResult.Failure -> { + consoleLogger.e( + "AlgebraExpTrivialManips", + "Encountered expression that failed parsing. Expression: $rawExpression." + + " Failure: ${expResult.error}." + ) + null + } + } + } + + private companion object { + private fun ClassificationContext.extractAllowedVariables(): List { + return customizationArgs["customOskLetters"] + ?.schemaObjectList + ?.schemaObjectList + ?.map { it.normalizedString } + ?: listOf() + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt new file mode 100644 index 00000000000..091dc12e4e6 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt @@ -0,0 +1,36 @@ +package org.oppia.android.domain.classify.rules.algebraicexpressioninput + +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import dagger.multibindings.StringKey +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.FractionInputRules + +/** Module that binds rule classifiers corresponding to the algebraic expression input interaction. */ +@Module +class AlgebraicExpressionInputModule { + @Provides + @IntoMap + @StringKey("MatchesExactlyWith") + @FractionInputRules + internal fun provideAlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider( + classifierProvider: AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() + + @Provides + @IntoMap + @StringKey("MatchesUpToTrivialManipulations") + @FractionInputRules + internal fun provideAlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifier( + classifierProvider: AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() + + @Provides + @IntoMap + @StringKey("IsEquivalentTo") + @FractionInputRules + internal fun provideAlgebraicExpressionInputIsEquivalentToRuleClassifier( + classifierProvider: AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() +} From 783396f6998eddde37ff61d8c3f3662bb44c5e22 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 17:49:16 -0800 Subject: [PATCH 020/134] Add missing annotation for new interaction. --- .../domain/classify/rules/RuleQualifiers.kt | 57 +++++++++++++++---- .../NumericExpressionInputModule.kt | 8 +-- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt index 18b9d9faed7..a8965af13ae 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt @@ -2,42 +2,79 @@ package org.oppia.android.domain.classify.rules import javax.inject.Qualifier -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the continue interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the + * continue interaction. + */ @Qualifier annotation class ContinueRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the fraction input interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the + * fraction input interaction. + */ @Qualifier annotation class FractionInputRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the item selection interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the item + * selection interaction. + */ @Qualifier annotation class ItemSelectionInputRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the multiple choice interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the + * multiple choice interaction. + */ @Qualifier annotation class MultipleChoiceInputRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the number with units interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the number + * with units interaction. + */ @Qualifier annotation class NumberWithUnitsRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the text input interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the text + * input interaction. + */ @Qualifier annotation class TextInputRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the numeric input interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the + * numeric input interaction. + */ @Qualifier annotation class NumericInputRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the drag drop sort input interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the drag + * drop sort input interaction. + */ @Qualifier annotation class DragDropSortInputRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the image click input interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the image + * click input interaction. + */ @Qualifier annotation class ImageClickInputRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the ratio input interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the ratio + * input interaction. + */ @Qualifier annotation class RatioExpressionInputRules + +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the + * numeric expression input interaction. + */ +@Qualifier +annotation class NumericExpressionInputRules diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt index cb010da48de..4a74256a777 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt @@ -5,7 +5,7 @@ import dagger.Provides import dagger.multibindings.IntoMap import dagger.multibindings.StringKey import org.oppia.android.domain.classify.RuleClassifier -import org.oppia.android.domain.classify.rules.FractionInputRules +import org.oppia.android.domain.classify.rules.NumericExpressionInputRules /** Module that binds rule classifiers corresponding to the numeric expression input interaction. */ @Module @@ -13,7 +13,7 @@ class NumericExpressionInputModule { @Provides @IntoMap @StringKey("MatchesExactlyWith") - @FractionInputRules + @NumericExpressionInputRules internal fun provideNumericExpressionInputMatchesExactlyWithRuleClassifierProvider( classifierProvider: NumericExpressionInputMatchesExactlyWithRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() @@ -21,7 +21,7 @@ class NumericExpressionInputModule { @Provides @IntoMap @StringKey("MatchesUpToTrivialManipulations") - @FractionInputRules + @NumericExpressionInputRules internal fun provideNumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifier( classifierProvider: NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() @@ -29,7 +29,7 @@ class NumericExpressionInputModule { @Provides @IntoMap @StringKey("IsEquivalentTo") - @FractionInputRules + @NumericExpressionInputRules internal fun provideNumericExpressionInputIsEquivalentToRuleClassifier( classifierProvider: NumericExpressionInputIsEquivalentToRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() From 69eaf25db8971d0bb8ae0539de09963714e8398e Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 17:50:36 -0800 Subject: [PATCH 021/134] Add missing annotation for new interaction. --- .../oppia/android/domain/classify/rules/RuleQualifiers.kt | 7 +++++++ .../AlgebraicExpressionInputModule.kt | 8 ++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt index a8965af13ae..9c38cbcef98 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt @@ -78,3 +78,10 @@ annotation class RatioExpressionInputRules */ @Qualifier annotation class NumericExpressionInputRules + +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the + * algebraic expression input interaction. + */ +@Qualifier +annotation class AlgebraicExpressionInputRules diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt index 091dc12e4e6..88019af6562 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt @@ -5,7 +5,7 @@ import dagger.Provides import dagger.multibindings.IntoMap import dagger.multibindings.StringKey import org.oppia.android.domain.classify.RuleClassifier -import org.oppia.android.domain.classify.rules.FractionInputRules +import org.oppia.android.domain.classify.rules.AlgebraicExpressionInputRules /** Module that binds rule classifiers corresponding to the algebraic expression input interaction. */ @Module @@ -13,7 +13,7 @@ class AlgebraicExpressionInputModule { @Provides @IntoMap @StringKey("MatchesExactlyWith") - @FractionInputRules + @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider( classifierProvider: AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() @@ -21,7 +21,7 @@ class AlgebraicExpressionInputModule { @Provides @IntoMap @StringKey("MatchesUpToTrivialManipulations") - @FractionInputRules + @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifier( classifierProvider: AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() @@ -29,7 +29,7 @@ class AlgebraicExpressionInputModule { @Provides @IntoMap @StringKey("IsEquivalentTo") - @FractionInputRules + @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputIsEquivalentToRuleClassifier( classifierProvider: AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() From 86256d948c60be6510022ee76af6983b704a7ddf Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 17:51:38 -0800 Subject: [PATCH 022/134] Lint fixes. --- ...cExpressionInputIsEquivalentToRuleClassifierProvider.kt | 2 +- ...ressionInputMatchesExactlyWithRuleClassifierProvider.kt | 5 +++-- ...atchesUpToTrivialManipulationsRuleClassifierProvider.kt | 5 +++-- .../AlgebraicExpressionInputModule.kt | 7 +++++-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt index 899a14682d6..276e9de2868 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -8,9 +8,9 @@ import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression import org.oppia.android.util.math.toPolynomial import javax.inject.Inject -import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression class AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt index fb521383d4c..b23d434d2d6 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -8,8 +8,8 @@ import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult -import javax.inject.Inject import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression +import javax.inject.Inject class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -35,7 +35,8 @@ class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject c } private fun parseExpression( - rawExpression: String, allowedVariables: List + rawExpression: String, + allowedVariables: List ): MathExpression? { return when (val expResult = parseAlgebraicExpression(rawExpression, allowedVariables)) { is MathParsingResult.Success -> expResult.result diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index b56fe353d39..a7062556901 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -8,9 +8,9 @@ import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression import org.oppia.android.util.math.toComparableOperationList import javax.inject.Inject -import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider @Inject constructor( @@ -37,7 +37,8 @@ class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvi } private fun parseComparableOperationList( - rawExpression: String, allowedVariables: List + rawExpression: String, + allowedVariables: List ): ComparableOperationList? { return when (val expResult = parseAlgebraicExpression(rawExpression, allowedVariables)) { is MathParsingResult.Success -> expResult.result.toComparableOperationList() diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt index 88019af6562..810f868808a 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt @@ -7,7 +7,9 @@ import dagger.multibindings.StringKey import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.AlgebraicExpressionInputRules -/** Module that binds rule classifiers corresponding to the algebraic expression input interaction. */ +/** + * Module that binds rule classifiers corresponding to the algebraic expression input interaction. + */ @Module class AlgebraicExpressionInputModule { @Provides @@ -23,7 +25,8 @@ class AlgebraicExpressionInputModule { @StringKey("MatchesUpToTrivialManipulations") @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifier( - classifierProvider: AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider + classifierProvider: + AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() @Provides From 07dcc94fad4cdb60cade7da79d7a4c78a3620b53 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 18:07:05 -0800 Subject: [PATCH 023/134] Add math equation input classifiers. --- .../domain/classify/rules/RuleQualifiers.kt | 7 ++ .../AlgebraicExpressionInputModule.kt | 9 ++- ...putIsEquivalentToRuleClassifierProvider.kt | 78 +++++++++++++++++++ ...atchesExactlyWithRuleClassifierProvider.kt | 63 +++++++++++++++ ...vialManipulationsRuleClassifierProvider.kt | 71 +++++++++++++++++ .../MathEquationInputModule.kt | 37 +++++++++ 6 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModule.kt diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt index 9c38cbcef98..ec08463b944 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt @@ -85,3 +85,10 @@ annotation class NumericExpressionInputRules */ @Qualifier annotation class AlgebraicExpressionInputRules + +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the + * math equation input interaction. + */ +@Qualifier +annotation class MathEquationInputRules diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt index 810f868808a..bdff7180826 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt @@ -6,6 +6,9 @@ import dagger.multibindings.IntoMap import dagger.multibindings.StringKey import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.AlgebraicExpressionInputRules +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputIsEquivalentToRuleClassifierProvider +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputMatchesExactlyWithRuleClassifierProvider +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider /** * Module that binds rule classifiers corresponding to the algebraic expression input interaction. @@ -17,7 +20,7 @@ class AlgebraicExpressionInputModule { @StringKey("MatchesExactlyWith") @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider( - classifierProvider: AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider + classifierProvider: MathEquationInputMatchesExactlyWithRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() @Provides @@ -26,7 +29,7 @@ class AlgebraicExpressionInputModule { @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifier( classifierProvider: - AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider + MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() @Provides @@ -34,6 +37,6 @@ class AlgebraicExpressionInputModule { @StringKey("IsEquivalentTo") @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputIsEquivalentToRuleClassifier( - classifierProvider: AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider + classifierProvider: MathEquationInputIsEquivalentToRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt new file mode 100644 index 00000000000..07de228a104 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt @@ -0,0 +1,78 @@ +package org.oppia.android.domain.classify.rules.mathequationinput + +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.Polynomial +import org.oppia.android.domain.classify.ClassificationContext +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.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation +import org.oppia.android.util.math.toPolynomial +import javax.inject.Inject + +class MathEquationInputIsEquivalentToRuleClassifierProvider @Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + classificationContext: ClassificationContext + ): Boolean { + val allowedVariables = classificationContext.extractAllowedVariables() + val (answerLhs, answerRhs) = parsePolynomials(answer, allowedVariables) ?: return false + val (inputLhs, inputRhs) = parsePolynomials(input, allowedVariables) ?: return false + + // Sides may cross-match (i.e. it's fine to reorder around the '='). + return (answerLhs == inputLhs && answerRhs == inputRhs) || + (answerLhs == inputRhs && answerRhs == inputLhs) + } + + private fun parsePolynomials( + rawEquation: String, + allowedVariables: List + ): Pair? { + return when (val eqResult = parseAlgebraicEquation(rawEquation, allowedVariables)) { + is MathParsingResult.Success -> { + val lhsExp = eqResult.result.leftSide.toPolynomial() + val rhsExp = eqResult.result.rightSide.toPolynomial() + if (lhsExp != null && rhsExp != null) { + lhsExp to rhsExp + } else { + consoleLogger.w( + "AlgebraEqEquivalent", "Equation is not a supported polynomial: $rawEquation." + ) + null + } + } + is MathParsingResult.Failure -> { + consoleLogger.e( + "AlgebraEqEquivalent", + "Encountered equation that failed parsing. Equation: $rawEquation." + + " Failure: ${eqResult.error}." + ) + null + } + } + } + + private companion object { + private fun ClassificationContext.extractAllowedVariables(): List { + return customizationArgs["customOskLetters"] + ?.schemaObjectList + ?.schemaObjectList + ?.map { it.normalizedString } + ?: listOf() + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt new file mode 100644 index 00000000000..3bb7e3d875c --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt @@ -0,0 +1,63 @@ +package org.oppia.android.domain.classify.rules.mathequationinput + +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.MathEquation +import org.oppia.android.domain.classify.ClassificationContext +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.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation +import javax.inject.Inject + +class MathEquationInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + classificationContext: ClassificationContext + ): Boolean { + val allowedVariables = classificationContext.extractAllowedVariables() + val answerEquation = parseEquation(answer, allowedVariables) ?: return false + val inputEquation = parseEquation(input, allowedVariables) ?: return false + return answerEquation == inputEquation + } + + private fun parseEquation( + rawEquation: String, + allowedVariables: List + ): MathEquation? { + return when (val eqResult = parseAlgebraicEquation(rawEquation, allowedVariables)) { + is MathParsingResult.Success -> eqResult.result + is MathParsingResult.Failure -> { + consoleLogger.e( + "AlgebraEqMatchesExact", + "Encountered equation that failed parsing. Equation: $rawEquation." + + " Failure: ${eqResult.error}." + ) + null + } + } + } + + private companion object { + private fun ClassificationContext.extractAllowedVariables(): List { + return customizationArgs["customOskLetters"] + ?.schemaObjectList + ?.schemaObjectList + ?.map { it.normalizedString } + ?: listOf() + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt new file mode 100644 index 00000000000..64b5e6215f6 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -0,0 +1,71 @@ +package org.oppia.android.domain.classify.rules.mathequationinput + +import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.domain.classify.ClassificationContext +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.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation +import org.oppia.android.util.math.toComparableOperationList +import javax.inject.Inject + +class MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider +@Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + classificationContext: ClassificationContext + ): Boolean { + val allowedVariables = classificationContext.extractAllowedVariables() + val (answerLhs, answerRhs) = parseComparableLists(answer, allowedVariables) ?: return false + val (inputLhs, inputRhs) = parseComparableLists(input, allowedVariables) ?: return false + + // Sides must match (reordering around the '=' is not allowed by this classifier). + return answerLhs == inputLhs && answerRhs == inputRhs + } + + private fun parseComparableLists( + rawEquation: String, + allowedVariables: List + ): Pair? { + return when (val eqResult = parseAlgebraicEquation(rawEquation, allowedVariables)) { + is MathParsingResult.Success -> { + val lhsExp = eqResult.result.leftSide + val rhsExp = eqResult.result.rightSide + lhsExp.toComparableOperationList() to rhsExp.toComparableOperationList() + } + is MathParsingResult.Failure -> { + consoleLogger.e( + "AlgebraEqTrivialManips", + "Encountered equation that failed parsing. Equation: $rawEquation." + + " Failure: ${eqResult.error}." + ) + null + } + } + } + + private companion object { + private fun ClassificationContext.extractAllowedVariables(): List { + return customizationArgs["customOskLetters"] + ?.schemaObjectList + ?.schemaObjectList + ?.map { it.normalizedString } + ?: listOf() + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModule.kt new file mode 100644 index 00000000000..8e8bc03f75d --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModule.kt @@ -0,0 +1,37 @@ +package org.oppia.android.domain.classify.rules.mathequationinput + +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import dagger.multibindings.StringKey +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.MathEquationInputRules + +/** Module that binds rule classifiers corresponding to the math equation input interaction. */ +@Module +class MathEquationInputModule { + @Provides + @IntoMap + @StringKey("MatchesExactlyWith") + @MathEquationInputRules + internal fun provideMathEquationInputMatchesExactlyWithRuleClassifier( + classifierProvider: MathEquationInputMatchesExactlyWithRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() + + @Provides + @IntoMap + @StringKey("MatchesUpToTrivialManipulations") + @MathEquationInputRules + internal fun provideMathEquationInputMatchesUpToTrivialManipulationsRuleClassifier( + classifierProvider: + MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() + + @Provides + @IntoMap + @StringKey("IsEquivalentTo") + @MathEquationInputRules + internal fun provideMathEquationInputIsEquivalentToRuleClassifier( + classifierProvider: MathEquationInputIsEquivalentToRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() +} From 42684b9e2250a7a613835cd9cd75e0b89c636a85 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 18:07:36 -0800 Subject: [PATCH 024/134] Fix provider name. --- .../numericexpressioninput/NumericExpressionInputModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt index 4a74256a777..ca42ea19de0 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt @@ -14,7 +14,7 @@ class NumericExpressionInputModule { @IntoMap @StringKey("MatchesExactlyWith") @NumericExpressionInputRules - internal fun provideNumericExpressionInputMatchesExactlyWithRuleClassifierProvider( + internal fun provideNumericExpressionInputMatchesExactlyWithRuleClassifier( classifierProvider: NumericExpressionInputMatchesExactlyWithRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() From aff5bc69fb64c85c466379b3cfe8580ee7190877 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 18:08:11 -0800 Subject: [PATCH 025/134] Fix provider name. --- .../algebraicexpressioninput/AlgebraicExpressionInputModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt index 810f868808a..9923355076f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt @@ -16,7 +16,7 @@ class AlgebraicExpressionInputModule { @IntoMap @StringKey("MatchesExactlyWith") @AlgebraicExpressionInputRules - internal fun provideAlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider( + internal fun provideAlgebraicExpressionInputMatchesExactlyWithRuleClassifier( classifierProvider: AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() From ebd2073b864a1b0b00eeca5626f57d14d3233f9a Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 18:10:39 -0800 Subject: [PATCH 026/134] Fix module regression from earlier commit. --- .../AlgebraicExpressionInputModule.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt index 1fe035f285d..9923355076f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt @@ -6,9 +6,6 @@ import dagger.multibindings.IntoMap import dagger.multibindings.StringKey import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.AlgebraicExpressionInputRules -import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputIsEquivalentToRuleClassifierProvider -import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputMatchesExactlyWithRuleClassifierProvider -import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider /** * Module that binds rule classifiers corresponding to the algebraic expression input interaction. @@ -29,7 +26,7 @@ class AlgebraicExpressionInputModule { @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifier( classifierProvider: - MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider + AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() @Provides @@ -37,6 +34,6 @@ class AlgebraicExpressionInputModule { @StringKey("IsEquivalentTo") @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputIsEquivalentToRuleClassifier( - classifierProvider: MathEquationInputIsEquivalentToRuleClassifierProvider + classifierProvider: AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() } From 46b765567d194cfb0e2c7f616f0ae9bf238ac944 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 22:23:14 -0800 Subject: [PATCH 027/134] Enable new math classifiers. Also, add the classifiers' new modules to all affected tests, plus both the prod and instrumentation application components. --- .../app/application/ApplicationComponent.kt | 5 +++ .../AdministratorControlsActivityTest.kt | 7 ++++- .../AppVersionActivityTest.kt | 7 ++++- .../CompletedStoryListActivityTest.kt | 7 ++++- .../LessonThumbnailImageViewTest.kt | 7 ++++- .../ImageViewBindingAdaptersTest.kt | 7 ++++- .../databinding/MarginBindingAdaptersTest.kt | 7 ++++- ...StateAssemblerMarginBindingAdaptersTest.kt | 7 ++++- ...tateAssemblerPaddingBindingAdaptersTest.kt | 7 ++++- .../databinding/ViewBindingAdaptersTest.kt | 7 ++++- .../DeveloperOptionsActivityTest.kt | 7 ++++- .../DeveloperOptionsFragmentTest.kt | 7 ++++- .../MarkChaptersCompletedActivityTest.kt | 7 ++++- .../MarkChaptersCompletedFragmentTest.kt | 7 ++++- .../MarkStoriesCompletedActivityTest.kt | 7 ++++- .../MarkStoriesCompletedFragmentTest.kt | 7 ++++- .../MarkTopicsCompletedActivityTest.kt | 7 ++++- .../MarkTopicsCompletedFragmentTest.kt | 7 ++++- .../devoptions/ViewEventLogsActivityTest.kt | 7 ++++- .../devoptions/ViewEventLogsFragmentTest.kt | 7 ++++- .../ForceNetworkTypeActivityTest.kt | 7 ++++- .../ForceNetworkTypeFragmentTest.kt | 7 ++++- .../android/app/faq/FAQListFragmentTest.kt | 7 ++++- .../android/app/faq/FAQSingleActivityTest.kt | 7 ++++- .../android/app/faq/FaqListActivityTest.kt | 7 ++++- .../android/app/help/HelpActivityTest.kt | 7 ++++- .../android/app/help/HelpFragmentTest.kt | 7 ++++- .../android/app/home/HomeActivityTest.kt | 7 ++++- .../app/home/RecentlyPlayedFragmentTest.kt | 7 ++++- .../app/home/TopicSummaryViewModelTest.kt | 7 ++++- .../android/app/home/WelcomeViewModelTest.kt | 7 ++++- .../PromotedStoryListViewModelTest.kt | 7 ++++- .../PromotedStoryViewModelTest.kt | 7 ++++- .../mydownloads/MyDownloadsFragmentTest.kt | 7 ++++- .../app/onboarding/OnboardingActivityTest.kt | 7 ++++- .../app/onboarding/OnboardingFragmentTest.kt | 7 ++++- .../OngoingTopicListActivityTest.kt | 7 ++++- .../app/options/AppLanguageActivityTest.kt | 7 ++++- .../app/options/AppLanguageFragmentTest.kt | 7 ++++- .../app/options/AudioLanguageActivityTest.kt | 7 ++++- .../app/options/AudioLanguageFragmentTest.kt | 7 ++++- .../app/options/OptionsActivityTest.kt | 7 ++++- .../app/options/OptionsFragmentTest.kt | 7 ++++- .../options/ReadingTextSizeActivityTest.kt | 7 ++++- .../options/ReadingTextSizeFragmentTest.kt | 7 ++++- .../app/parser/CustomBulletSpanTest.kt | 7 ++++- .../android/app/parser/HtmlParserTest.kt | 7 ++++- .../app/player/audio/AudioFragmentTest.kt | 7 ++++- .../exploration/ExplorationActivityTest.kt | 7 ++++- .../app/player/state/StateFragmentTest.kt | 7 ++++- .../app/profile/AddProfileActivityTest.kt | 7 ++++- .../app/profile/AdminAuthActivityTest.kt | 7 ++++- .../app/profile/AdminPinActivityTest.kt | 7 ++++- .../app/profile/PinPasswordActivityTest.kt | 7 ++++- .../app/profile/ProfileChooserFragmentTest.kt | 7 ++++- .../ProfilePictureActivityTest.kt | 7 ++++- .../ProfileProgressActivityTest.kt | 7 ++++- .../ProfileProgressFragmentTest.kt | 7 ++++- .../app/recyclerview/BindableAdapterTest.kt | 7 ++++- .../resumelesson/ResumeLessonActivityTest.kt | 7 ++++- .../resumelesson/ResumeLessonFragmentTest.kt | 7 ++++- .../profile/ProfileEditActivityTest.kt | 7 ++++- .../profile/ProfileListActivityTest.kt | 7 ++++- .../profile/ProfileListFragmentTest.kt | 7 ++++- .../profile/ProfileRenameActivityTest.kt | 7 ++++- .../profile/ProfileRenameFragmentTest.kt | 7 ++++- .../profile/ProfileResetPinActivityTest.kt | 7 ++++- .../android/app/splash/SplashActivityTest.kt | 7 ++++- .../android/app/story/StoryActivityTest.kt | 7 ++++- .../android/app/story/StoryFragmentTest.kt | 7 ++++- .../app/testing/DragDropTestActivityTest.kt | 7 ++++- ...ImageRegionSelectionInteractionViewTest.kt | 7 ++++- .../InputInteractionViewTestActivityTest.kt | 7 ++++- .../NavigationDrawerActivityDebugTest.kt | 7 ++++- .../NavigationDrawerActivityProdTest.kt | 6 +++- ...tFontScaleConfigurationUtilActivityTest.kt | 7 ++++- .../testing/TopicTestActivityForStoryTest.kt | 7 ++++- .../app/thirdparty/LicenseListActivityTest.kt | 7 ++++- .../app/thirdparty/LicenseListFragmentTest.kt | 7 ++++- .../LicenseTextViewerActivityTest.kt | 7 ++++- .../LicenseTextViewerFragmentTest.kt | 7 ++++- .../ThirdPartyDependencyListActivityTest.kt | 7 ++++- .../ThirdPartyDependencyListFragmentTest.kt | 7 ++++- .../android/app/topic/TopicActivityTest.kt | 7 ++++- .../android/app/topic/TopicFragmentTest.kt | 7 ++++- .../conceptcard/ConceptCardFragmentTest.kt | 7 ++++- .../app/topic/info/TopicInfoFragmentTest.kt | 7 ++++- .../topic/lessons/TopicLessonsFragmentTest.kt | 7 ++++- .../practice/TopicPracticeFragmentTest.kt | 7 ++++- .../QuestionPlayerActivityTest.kt | 7 ++++- .../revision/TopicRevisionFragmentTest.kt | 7 ++++- .../revisioncard/RevisionCardActivityTest.kt | 7 ++++- .../revisioncard/RevisionCardFragmentTest.kt | 7 ++++- .../app/utility/RatioExtensionsTest.kt | 7 ++++- .../walkthrough/WalkthroughActivityTest.kt | 7 ++++- .../WalkthroughFinalFragmentTest.kt | 7 ++++- .../WalkthroughTopicListFragmentTest.kt | 7 ++++- .../WalkthroughWelcomeFragmentTest.kt | 7 ++++- .../activity/ActivityIntentFactoriesTest.kt | 7 ++++- .../android/app/home/HomeActivityLocalTest.kt | 6 +++- .../app/parser/StringToFractionParserTest.kt | 7 ++++- .../app/parser/StringToRatioParserTest.kt | 7 ++++- .../ExplorationActivityLocalTest.kt | 6 +++- .../player/state/StateFragmentLocalTest.kt | 7 ++++- .../ProfileChooserFragmentLocalTest.kt | 7 ++++- .../app/story/StoryActivityLocalTest.kt | 7 ++++- .../app/testing/CompletedStoryListSpanTest.kt | 6 +++- .../oppia/android/app/testing/HomeSpanTest.kt | 6 +++- .../app/testing/OngoingTopicListSpanTest.kt | 6 +++- .../PlatformParameterIntegrationTest.kt | 7 ++++- .../app/testing/ProfileChooserSpanTest.kt | 6 +++- .../app/testing/ProfileProgressSpanCount.kt | 6 +++- .../app/testing/RecentlyPlayedSpanTest.kt | 6 +++- .../app/testing/TopicRevisionSpanTest.kt | 6 +++- .../app/testing/activity/TestActivityTest.kt | 7 ++++- .../AdministratorControlsFragmentTest.kt | 7 ++++- .../testing/options/OptionsFragmentTest.kt | 7 ++++- .../player/split/PlayerSplitScreenTesting.kt | 6 +++- .../state/StateFragmentAccessibilityTest.kt | 6 +++- .../topic/info/TopicInfoFragmentLocalTest.kt | 7 ++++- .../lessons/TopicLessonsFragmentLocalTest.kt | 7 ++++- .../QuestionPlayerActivityLocalTest.kt | 7 ++++- .../RevisionCardActivityLocalTest.kt | 7 ++++- .../AppLanguageResourceHandlerTest.kt | 7 ++++- .../AppLanguageWatcherMixinTest.kt | 7 ++++- .../app/utility/datetime/DateTimeUtilTest.kt | 7 ++++- .../domain/classify/InteractionsModule.kt | 31 +++++++++++++++++++ .../AnswerClassificationControllerTest.kt | 7 ++++- .../ExplorationDataControllerTest.kt | 6 +++- .../ExplorationProgressControllerTest.kt | 6 +++- ...uestionAssessmentProgressControllerTest.kt | 6 +++- .../QuestionTrainingControllerTest.kt | 7 ++++- .../application/TestApplicationComponent.kt | 5 +++ ...alizeDefaultLocaleRuleCustomContextTest.kt | 7 ++++- ...InitializeDefaultLocaleRuleOmissionTest.kt | 7 ++++- .../junit/InitializeDefaultLocaleRuleTest.kt | 7 ++++- 136 files changed, 824 insertions(+), 133 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt b/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt index dbb6b7dd5d8..0f708fef5f8 100644 --- a/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt +++ b/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt @@ -14,13 +14,16 @@ import org.oppia.android.app.translation.ActivityRecreatorProdModule 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -93,6 +96,8 @@ import javax.inject.Singleton DeveloperOptionsModule::class, PlatformParameterSyncUpWorkerModule::class, NetworkConnectionUtilDebugModule::class, NetworkConfigProdModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorProdModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class, // TODO(#59): Remove this 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. NetworkConnectionDebugUtilModule::class diff --git a/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsActivityTest.kt index d88d63e1650..159cce60276 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsActivityTest.kt @@ -68,13 +68,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -719,7 +722,9 @@ class AdministratorControlsActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AppVersionActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AppVersionActivityTest.kt index 801af1bccff..6922b83fb3d 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AppVersionActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AppVersionActivityTest.kt @@ -48,13 +48,16 @@ import org.oppia.android.app.utility.getVersionName 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -279,7 +282,9 @@ class AppVersionActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivityTest.kt index 3ac470b78ad..d5117f9b248 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivityTest.kt @@ -49,13 +49,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -502,7 +505,9 @@ class CompletedStoryListActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/customview/LessonThumbnailImageViewTest.kt b/app/src/sharedTest/java/org/oppia/android/app/customview/LessonThumbnailImageViewTest.kt index 8b577dbc71d..01b3c5a1ee2 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/customview/LessonThumbnailImageViewTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/customview/LessonThumbnailImageViewTest.kt @@ -32,13 +32,16 @@ import org.oppia.android.app.utility.EspressoTestsMatchers.withDrawable 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -160,7 +163,9 @@ class LessonThumbnailImageViewTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/ImageViewBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/ImageViewBindingAdaptersTest.kt index d342a5e559d..b15930dedaf 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/ImageViewBindingAdaptersTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/ImageViewBindingAdaptersTest.kt @@ -42,13 +42,16 @@ import org.oppia.android.app.utility.EspressoTestsMatchers.withDrawable 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -209,7 +212,9 @@ class ImageViewBindingAdaptersTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) /** Create a TestApplicationComponent. */ diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/MarginBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/MarginBindingAdaptersTest.kt index c079fd4dda3..bb2a7fe3506 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/MarginBindingAdaptersTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/MarginBindingAdaptersTest.kt @@ -43,13 +43,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -297,7 +300,9 @@ class MarginBindingAdaptersTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) /** Create a TestApplicationComponent. */ diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerMarginBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerMarginBindingAdaptersTest.kt index 270c740a1d2..080a013ee66 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerMarginBindingAdaptersTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerMarginBindingAdaptersTest.kt @@ -45,13 +45,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -485,7 +488,9 @@ class StateAssemblerMarginBindingAdaptersTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) /** Create a TestApplicationComponent. */ diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerPaddingBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerPaddingBindingAdaptersTest.kt index ddcd62043c1..2d1f8204539 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerPaddingBindingAdaptersTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerPaddingBindingAdaptersTest.kt @@ -43,13 +43,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -483,7 +486,9 @@ class StateAssemblerPaddingBindingAdaptersTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/ViewBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/ViewBindingAdaptersTest.kt index dcfb063a37d..e01404ce617 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/ViewBindingAdaptersTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/ViewBindingAdaptersTest.kt @@ -40,13 +40,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -217,7 +220,9 @@ class ViewBindingAdaptersTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) /** Create a TestApplicationComponent. */ diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsActivityTest.kt index fd2c18b6502..ddb40edd5a5 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsActivityTest.kt @@ -52,13 +52,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -285,7 +288,9 @@ class DeveloperOptionsActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsFragmentTest.kt index 379f01c84f8..1443f901790 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsFragmentTest.kt @@ -52,13 +52,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -613,7 +616,9 @@ class DeveloperOptionsFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedActivityTest.kt index d5e639ba1d8..ee5330a99a6 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedActivityTest.kt @@ -35,13 +35,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -175,7 +178,9 @@ class MarkChaptersCompletedActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedFragmentTest.kt index 7505976ed81..2e966c81b4a 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedFragmentTest.kt @@ -44,13 +44,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -879,7 +882,9 @@ class MarkChaptersCompletedFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedActivityTest.kt index 57b31d34318..fed5ad8a6ba 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedActivityTest.kt @@ -35,13 +35,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -175,7 +178,9 @@ class MarkStoriesCompletedActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedFragmentTest.kt index c29d95f046c..2385222bfb9 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedFragmentTest.kt @@ -44,13 +44,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -580,7 +583,9 @@ class MarkStoriesCompletedFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedActivityTest.kt index 9e7bd458c57..4ab74ccc9ef 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedActivityTest.kt @@ -35,13 +35,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -175,7 +178,9 @@ class MarkTopicsCompletedActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedFragmentTest.kt index 26c9d35446f..1ba8cf8aa45 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedFragmentTest.kt @@ -44,13 +44,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -550,7 +553,9 @@ class MarkTopicsCompletedFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsActivityTest.kt index 50a7fb1341e..52876fb584b 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsActivityTest.kt @@ -36,13 +36,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -163,7 +166,9 @@ class ViewEventLogsActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsFragmentTest.kt index cf5fbb8c656..39d046d1bd8 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsFragmentTest.kt @@ -42,13 +42,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -619,7 +622,9 @@ class ViewEventLogsFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeActivityTest.kt index 852966ba144..91f7b1aab13 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeActivityTest.kt @@ -36,13 +36,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -167,7 +170,9 @@ class ForceNetworkTypeActivityTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) /** [ApplicationComponent] for [ForceNetworkTypeActivityTest]. */ diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeFragmentTest.kt index 3d42ca09eb8..09d87f436f1 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeFragmentTest.kt @@ -41,13 +41,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -383,7 +386,9 @@ class ForceNetworkTypeFragmentTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) /** [ApplicationComponent] for [ForceNetworkTypeFragmentTest]. */ diff --git a/app/src/sharedTest/java/org/oppia/android/app/faq/FAQListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/faq/FAQListFragmentTest.kt index df66cb56cd9..0175df9bc05 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/faq/FAQListFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/faq/FAQListFragmentTest.kt @@ -47,13 +47,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -235,7 +238,9 @@ class FAQListFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/faq/FAQSingleActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/faq/FAQSingleActivityTest.kt index e7dfe213e43..c6b1bdb48e6 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/faq/FAQSingleActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/faq/FAQSingleActivityTest.kt @@ -41,13 +41,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -207,7 +210,9 @@ class FAQSingleActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/faq/FaqListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/faq/FaqListActivityTest.kt index 28f0cb11667..d217168b930 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/faq/FaqListActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/faq/FaqListActivityTest.kt @@ -30,13 +30,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -140,7 +143,9 @@ class FaqListActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/help/HelpActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/help/HelpActivityTest.kt index 69bad4b72cc..48375856775 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/help/HelpActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/help/HelpActivityTest.kt @@ -29,13 +29,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -142,7 +145,9 @@ class HelpActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, NetworkModule::class, ExplorationStorageModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/help/HelpFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/help/HelpFragmentTest.kt index 2edef7165b0..7d45ce45b5c 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/help/HelpFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/help/HelpFragmentTest.kt @@ -54,13 +54,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -1180,7 +1183,9 @@ class HelpFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt index d60ec860aec..388b7d6ae21 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt @@ -76,13 +76,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -1745,7 +1748,9 @@ class HomeActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/RecentlyPlayedFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/RecentlyPlayedFragmentTest.kt index 0a525d1c9a9..ed091721a70 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/home/RecentlyPlayedFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/home/RecentlyPlayedFragmentTest.kt @@ -63,13 +63,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -1494,7 +1497,9 @@ class RecentlyPlayedFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/TopicSummaryViewModelTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/TopicSummaryViewModelTest.kt index 958bf9b1f15..47af9a09fb0 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/home/TopicSummaryViewModelTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/home/TopicSummaryViewModelTest.kt @@ -31,13 +31,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -361,7 +364,9 @@ class TopicSummaryViewModelTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class, PlatformParameterSingletonModule::class + NetworkConfigProdModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/WelcomeViewModelTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/WelcomeViewModelTest.kt index 65a83551f3e..4cd7decfaf1 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/home/WelcomeViewModelTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/home/WelcomeViewModelTest.kt @@ -31,13 +31,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -345,7 +348,9 @@ class WelcomeViewModelTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class, PlatformParameterSingletonModule::class + NetworkConfigProdModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryListViewModelTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryListViewModelTest.kt index 13894d9a2d4..75ec22943e1 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryListViewModelTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryListViewModelTest.kt @@ -32,13 +32,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -357,7 +360,9 @@ class PromotedStoryListViewModelTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class, PlatformParameterSingletonModule::class + NetworkConfigProdModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryViewModelTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryViewModelTest.kt index 1313cc9c660..3095b208e65 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryViewModelTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryViewModelTest.kt @@ -30,13 +30,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -367,7 +370,9 @@ class PromotedStoryViewModelTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class, PlatformParameterSingletonModule::class + NetworkConfigProdModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsFragmentTest.kt index 662eeef51a6..4e218635d4d 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsFragmentTest.kt @@ -39,13 +39,16 @@ import org.oppia.android.app.utility.EspressoTestsMatchers.matchCurrentTabTitle 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -221,7 +224,9 @@ class MyDownloadsFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingActivityTest.kt index af6b7269724..91b9c1164e5 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingActivityTest.kt @@ -29,13 +29,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -139,7 +142,9 @@ class OnboardingActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt index 87389344071..3f6b2105c76 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt @@ -56,13 +56,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -682,7 +685,9 @@ class OnboardingFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListActivityTest.kt index 171f1b0e8af..396c3c2c8ae 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListActivityTest.kt @@ -48,13 +48,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -446,7 +449,9 @@ class OngoingTopicListActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageActivityTest.kt index 03da5a775d5..8b5e9873e87 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageActivityTest.kt @@ -29,13 +29,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -149,7 +152,9 @@ class AppLanguageActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageFragmentTest.kt index 254e35c9113..df2e7442cae 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageFragmentTest.kt @@ -38,13 +38,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -244,7 +247,9 @@ class AppLanguageFragmentTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class, PlatformParameterSingletonModule::class + NetworkConfigProdModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageActivityTest.kt index 7ecf36b535d..e91975c33d6 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageActivityTest.kt @@ -29,13 +29,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -149,7 +152,9 @@ class AudioLanguageActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageFragmentTest.kt index 66de1647b23..856ee69f349 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageFragmentTest.kt @@ -37,13 +37,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -237,7 +240,9 @@ class AudioLanguageFragmentTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class, PlatformParameterSingletonModule::class + NetworkConfigProdModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsActivityTest.kt index 5efa873b07c..09492b1bad0 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsActivityTest.kt @@ -29,13 +29,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -141,7 +144,9 @@ class OptionsActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt index d6339898f06..246a0929c01 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt @@ -51,13 +51,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -674,7 +677,9 @@ class OptionsFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeActivityTest.kt index 5cd7267de8a..37f49d2f52c 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeActivityTest.kt @@ -29,13 +29,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -149,7 +152,9 @@ class ReadingTextSizeActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt index 566c462e0db..08d958b81ba 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt @@ -44,13 +44,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -303,7 +306,9 @@ class ReadingTextSizeFragmentTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class, PlatformParameterSingletonModule::class + NetworkConfigProdModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/parser/CustomBulletSpanTest.kt b/app/src/sharedTest/java/org/oppia/android/app/parser/CustomBulletSpanTest.kt index ed7afe12eb6..159218cf20c 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/parser/CustomBulletSpanTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/parser/CustomBulletSpanTest.kt @@ -31,13 +31,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -241,7 +244,9 @@ class CustomBulletSpanTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class, PlatformParameterSingletonModule::class + NetworkConfigProdModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt b/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt index 2c95c3add3b..6362346e0fa 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt @@ -57,13 +57,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -643,7 +646,9 @@ class HtmlParserTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/audio/AudioFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/audio/AudioFragmentTest.kt index 50fbd545bdb..5f803b1f60a 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/player/audio/AudioFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/player/audio/AudioFragmentTest.kt @@ -53,13 +53,16 @@ import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.audio.AudioPlayerController import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -470,7 +473,9 @@ class AudioFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt index e79c1a441ce..16868b4f728 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt @@ -84,13 +84,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -2037,7 +2040,9 @@ class ExplorationActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, TestExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt index 6c0917f22b4..2fd6c8b24fd 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt @@ -103,13 +103,16 @@ import org.oppia.android.app.utility.clickPoint 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -2874,7 +2877,9 @@ class StateFragmentTest { ExplorationStorageModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, NetworkModule::class, NetworkConfigProdModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/AddProfileActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/AddProfileActivityTest.kt index 9ddc39c067d..021448bad2b 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profile/AddProfileActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profile/AddProfileActivityTest.kt @@ -63,13 +63,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -1711,7 +1714,9 @@ class AddProfileActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/AdminAuthActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/AdminAuthActivityTest.kt index f8638be04a1..a666d92be7b 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profile/AdminAuthActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profile/AdminAuthActivityTest.kt @@ -49,13 +49,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -660,7 +663,9 @@ class AdminAuthActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/AdminPinActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/AdminPinActivityTest.kt index b028e931d58..1aa691c4a82 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profile/AdminPinActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profile/AdminPinActivityTest.kt @@ -58,13 +58,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -1078,7 +1081,9 @@ class AdminPinActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/PinPasswordActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/PinPasswordActivityTest.kt index 1b2943a2d3f..4578cd593b3 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profile/PinPasswordActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profile/PinPasswordActivityTest.kt @@ -52,13 +52,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -1151,7 +1154,9 @@ class PinPasswordActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt index 7138aac599c..aea8c488ca5 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt @@ -51,13 +51,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -518,7 +521,9 @@ class ProfileChooserFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfilePictureActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfilePictureActivityTest.kt index feea5e9b922..701b630d964 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfilePictureActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfilePictureActivityTest.kt @@ -37,13 +37,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -192,7 +195,9 @@ class ProfilePictureActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressActivityTest.kt index 8284f76cf64..4f2f42f5912 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressActivityTest.kt @@ -29,13 +29,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -143,7 +146,9 @@ class ProfileProgressActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentTest.kt index d7c34f86734..fe7ff24d608 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentTest.kt @@ -69,13 +69,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -844,7 +847,9 @@ class ProfileProgressFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/recyclerview/BindableAdapterTest.kt b/app/src/sharedTest/java/org/oppia/android/app/recyclerview/BindableAdapterTest.kt index 8ca7d3dd9ba..e12136e90cd 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/recyclerview/BindableAdapterTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/recyclerview/BindableAdapterTest.kt @@ -68,13 +68,16 @@ import org.oppia.android.databinding.TestTextViewForIntWithDataBindingBinding import org.oppia.android.databinding.TestTextViewForLiveDataWithDataBindingBinding import org.oppia.android.databinding.TestTextViewForStringWithDataBindingBinding import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -756,7 +759,9 @@ class BindableAdapterTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonActivityTest.kt index 70e1c86d40d..17bbe1ad2b9 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonActivityTest.kt @@ -44,13 +44,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -213,7 +216,9 @@ class ResumeLessonActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentTest.kt index 93fa9266efc..ef1954c639e 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentTest.kt @@ -42,13 +42,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -275,7 +278,9 @@ class ResumeLessonFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditActivityTest.kt index 3c11f43e592..7f351099569 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditActivityTest.kt @@ -49,13 +49,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -565,7 +568,9 @@ class ProfileEditActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListActivityTest.kt index 98b36aa1f3a..e4a8d7d8ba6 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListActivityTest.kt @@ -29,13 +29,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -139,7 +142,9 @@ class ProfileListActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListFragmentTest.kt index 841f849c063..5e6478efe3e 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListFragmentTest.kt @@ -44,13 +44,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -377,7 +380,9 @@ class ProfileListFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameActivityTest.kt index 16c2eb4085a..794efaf80ee 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameActivityTest.kt @@ -30,13 +30,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -162,7 +165,9 @@ class ProfileRenameActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentTest.kt index ca237fbd195..a9ecb8e5861 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentTest.kt @@ -46,13 +46,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -451,7 +454,9 @@ class ProfileRenameFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinActivityTest.kt index f36a2a3a2a2..b3e697a860e 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinActivityTest.kt @@ -49,13 +49,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -1040,7 +1043,9 @@ class ProfileResetPinActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt index 4dfa5800e92..d1c617371da 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt @@ -50,13 +50,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -481,7 +484,9 @@ class SplashActivityTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/story/StoryActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/story/StoryActivityTest.kt index b0e77c6d24f..e8a9327bc5d 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/story/StoryActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/story/StoryActivityTest.kt @@ -43,13 +43,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -223,7 +226,9 @@ class StoryActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt index e056e5d7dab..d1c7ccef311 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt @@ -77,13 +77,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -887,7 +890,9 @@ class StoryFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/DragDropTestActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/DragDropTestActivityTest.kt index fe7173623e8..bcb8f2a1cf8 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/testing/DragDropTestActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/testing/DragDropTestActivityTest.kt @@ -40,13 +40,16 @@ import org.oppia.android.app.utility.RecyclerViewCoordinatesProvider 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -212,7 +215,9 @@ class DragDropTestActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/ImageRegionSelectionInteractionViewTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/ImageRegionSelectionInteractionViewTest.kt index 3f12a269df1..9e3bfe67c80 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/testing/ImageRegionSelectionInteractionViewTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/testing/ImageRegionSelectionInteractionViewTest.kt @@ -51,13 +51,16 @@ import org.oppia.android.app.utility.clickPoint 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -379,7 +382,9 @@ class ImageRegionSelectionInteractionViewTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { 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 d9100849877..9b7437a6d02 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 @@ -45,13 +45,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -1028,7 +1031,9 @@ class InputInteractionViewTestActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityDebugTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityDebugTest.kt index 9d23dd7c2d5..95365580884 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityDebugTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityDebugTest.kt @@ -66,13 +66,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -431,7 +434,9 @@ class NavigationDrawerActivityDebugTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityProdTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityProdTest.kt index f98a53fd6bc..32761957182 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityProdTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityProdTest.kt @@ -74,13 +74,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -980,7 +983,8 @@ class NavigationDrawerActivityProdTest { DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivityTest.kt index 5ad3aa73abc..4536b24af0f 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivityTest.kt @@ -35,13 +35,16 @@ import org.oppia.android.app.utility.FontSizeMatcher.Companion.withFontSize 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -190,7 +193,9 @@ class TestFontScaleConfigurationUtilActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/TopicTestActivityForStoryTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/TopicTestActivityForStoryTest.kt index 7e808fe423f..c706b9d6bae 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/testing/TopicTestActivityForStoryTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/testing/TopicTestActivityForStoryTest.kt @@ -41,13 +41,16 @@ import org.oppia.android.app.utility.EspressoTestsMatchers.matchCurrentTabTitle 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -187,7 +190,9 @@ class TopicTestActivityForStoryTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListActivityTest.kt index 64b472286ea..9c108b1aed1 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListActivityTest.kt @@ -30,13 +30,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -151,7 +154,9 @@ class LicenseListActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListFragmentTest.kt index ef451afb505..ebdec396258 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListFragmentTest.kt @@ -46,13 +46,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -365,7 +368,9 @@ class LicenseListFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerActivityTest.kt index 9b55e0f23df..2f296d0a7dc 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerActivityTest.kt @@ -31,13 +31,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -160,7 +163,9 @@ class LicenseTextViewerActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerFragmentTest.kt index c4f6c6789b5..4a9ab14597c 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerFragmentTest.kt @@ -36,13 +36,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -343,7 +346,9 @@ class LicenseTextViewerFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListActivityTest.kt index 375e562c6b8..9de615de34c 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListActivityTest.kt @@ -30,13 +30,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -148,7 +151,9 @@ class ThirdPartyDependencyListActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListFragmentTest.kt index 87e71a94ddb..55d903219bd 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListFragmentTest.kt @@ -45,13 +45,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -475,7 +478,9 @@ class ThirdPartyDependencyListFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/TopicActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/TopicActivityTest.kt index a3cb999be4d..1a41429def0 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/TopicActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/TopicActivityTest.kt @@ -42,13 +42,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -197,7 +200,9 @@ class TopicActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/TopicFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/TopicFragmentTest.kt index 551141b4af3..5f47407774b 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/TopicFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/TopicFragmentTest.kt @@ -53,13 +53,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -652,7 +655,9 @@ class TopicFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentTest.kt index 7de3ff0569c..0baa3f515a0 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentTest.kt @@ -51,13 +51,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -427,7 +430,9 @@ class ConceptCardFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/info/TopicInfoFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/info/TopicInfoFragmentTest.kt index 2ab597a75d1..42cd11e3ed3 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/info/TopicInfoFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/info/TopicInfoFragmentTest.kt @@ -55,13 +55,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -483,7 +486,9 @@ class TopicInfoFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt index 2c4251fd0f2..18a483907d1 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt @@ -61,13 +61,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -1000,7 +1003,9 @@ class TopicLessonsFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentTest.kt index 56fa8cb4e05..ce2bebc8bf4 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentTest.kt @@ -52,13 +52,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -424,7 +427,9 @@ class TopicPracticeFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityTest.kt index 6d3d747fe1d..c0a3f4595c0 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityTest.kt @@ -75,13 +75,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -719,7 +722,9 @@ class QuestionPlayerActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentTest.kt index e8e933c21ce..ab3d409fece 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentTest.kt @@ -53,13 +53,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -320,7 +323,9 @@ class TopicRevisionFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityTest.kt index d08e2a61010..2ae6f908dee 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityTest.kt @@ -43,13 +43,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -278,7 +281,9 @@ class RevisionCardActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt index 95f22c51025..8bff4e25299 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt @@ -61,13 +61,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -590,7 +593,9 @@ class RevisionCardFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/utility/RatioExtensionsTest.kt b/app/src/sharedTest/java/org/oppia/android/app/utility/RatioExtensionsTest.kt index 2260eace947..ff080b94a34 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/utility/RatioExtensionsTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/utility/RatioExtensionsTest.kt @@ -28,13 +28,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -143,7 +146,9 @@ class RatioExtensionsTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughActivityTest.kt index 9f4fcfa2482..cd00f8aa28b 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughActivityTest.kt @@ -37,13 +37,16 @@ import org.oppia.android.app.utility.ProgressMatcher.Companion.withProgress 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -195,7 +198,9 @@ class WalkthroughActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughFinalFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughFinalFragmentTest.kt index 6cf37058cad..ee00467d323 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughFinalFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughFinalFragmentTest.kt @@ -44,13 +44,16 @@ import org.oppia.android.app.utility.ProgressMatcher.Companion.withProgress 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -280,7 +283,9 @@ class WalkthroughFinalFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughTopicListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughTopicListFragmentTest.kt index a535b248c50..21ff6100425 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughTopicListFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughTopicListFragmentTest.kt @@ -45,13 +45,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -306,7 +309,9 @@ class WalkthroughTopicListFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughWelcomeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughWelcomeFragmentTest.kt index 849c5b61ce3..0a39884dd55 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughWelcomeFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughWelcomeFragmentTest.kt @@ -40,13 +40,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -203,7 +206,9 @@ class WalkthroughWelcomeFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/activity/ActivityIntentFactoriesTest.kt b/app/src/test/java/org/oppia/android/app/activity/ActivityIntentFactoriesTest.kt index 4331cd3e682..9a145af0a7e 100644 --- a/app/src/test/java/org/oppia/android/app/activity/ActivityIntentFactoriesTest.kt +++ b/app/src/test/java/org/oppia/android/app/activity/ActivityIntentFactoriesTest.kt @@ -31,13 +31,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -170,7 +173,9 @@ class ActivityIntentFactoriesTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - ActivityIntentFactoriesModule::class, PlatformParameterSingletonModule::class + ActivityIntentFactoriesModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/home/HomeActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/home/HomeActivityLocalTest.kt index b4cbbcd59d9..8e62d7024a1 100644 --- a/app/src/test/java/org/oppia/android/app/home/HomeActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/home/HomeActivityLocalTest.kt @@ -32,13 +32,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -143,7 +146,8 @@ class HomeActivityLocalTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { 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 index 1d71c8c8afd..2e0be2a1d38 100644 --- a/app/src/test/java/org/oppia/android/app/parser/StringToFractionParserTest.kt +++ b/app/src/test/java/org/oppia/android/app/parser/StringToFractionParserTest.kt @@ -28,13 +28,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -479,7 +482,9 @@ class StringToFractionParserTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/parser/StringToRatioParserTest.kt b/app/src/test/java/org/oppia/android/app/parser/StringToRatioParserTest.kt index 57867f5f370..9ece48261e7 100644 --- a/app/src/test/java/org/oppia/android/app/parser/StringToRatioParserTest.kt +++ b/app/src/test/java/org/oppia/android/app/parser/StringToRatioParserTest.kt @@ -28,13 +28,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -256,7 +259,9 @@ class StringToRatioParserTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt index 6ff810db3a9..c721dce2972 100644 --- a/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt @@ -33,13 +33,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -211,7 +214,8 @@ class ExplorationActivityLocalTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt index dc0ff28e020..4dc01162509 100644 --- a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt @@ -87,13 +87,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -2061,7 +2064,9 @@ class StateFragmentLocalTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/profile/ProfileChooserFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/profile/ProfileChooserFragmentLocalTest.kt index 3c9833ab88c..08f6ac552f3 100644 --- a/app/src/test/java/org/oppia/android/app/profile/ProfileChooserFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/profile/ProfileChooserFragmentLocalTest.kt @@ -30,13 +30,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -133,7 +136,9 @@ class ProfileChooserFragmentLocalTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/story/StoryActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/story/StoryActivityLocalTest.kt index 05458296564..ccb38b584c9 100644 --- a/app/src/test/java/org/oppia/android/app/story/StoryActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/story/StoryActivityLocalTest.kt @@ -31,13 +31,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -157,7 +160,9 @@ class StoryActivityLocalTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/CompletedStoryListSpanTest.kt b/app/src/test/java/org/oppia/android/app/testing/CompletedStoryListSpanTest.kt index 1080c3c329b..d9d447ac23f 100644 --- a/app/src/test/java/org/oppia/android/app/testing/CompletedStoryListSpanTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/CompletedStoryListSpanTest.kt @@ -33,13 +33,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -165,7 +168,8 @@ class CompletedStoryListSpanTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/HomeSpanTest.kt b/app/src/test/java/org/oppia/android/app/testing/HomeSpanTest.kt index 91d1cb2e664..842529cd959 100644 --- a/app/src/test/java/org/oppia/android/app/testing/HomeSpanTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/HomeSpanTest.kt @@ -33,13 +33,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -179,7 +182,8 @@ class HomeSpanTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/OngoingTopicListSpanTest.kt b/app/src/test/java/org/oppia/android/app/testing/OngoingTopicListSpanTest.kt index 370944949d3..ce592d56c0e 100644 --- a/app/src/test/java/org/oppia/android/app/testing/OngoingTopicListSpanTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/OngoingTopicListSpanTest.kt @@ -34,13 +34,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -176,7 +179,8 @@ class OngoingTopicListSpanTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/PlatformParameterIntegrationTest.kt b/app/src/test/java/org/oppia/android/app/testing/PlatformParameterIntegrationTest.kt index 73932a62756..3cad9b897a2 100644 --- a/app/src/test/java/org/oppia/android/app/testing/PlatformParameterIntegrationTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/PlatformParameterIntegrationTest.kt @@ -47,13 +47,16 @@ import org.oppia.android.data.backends.gae.OppiaRetrofit import org.oppia.android.data.backends.gae.RemoteAuthNetworkInterceptor import org.oppia.android.data.backends.gae.api.PlatformParameterService import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -349,7 +352,9 @@ class PlatformParameterIntegrationTest { ExplorationStorageModule::class, TestNetworkModule::class, RetrofitTestModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, - ActivityRecreatorTestModule::class, PlatformParameterSingletonModule::class + ActivityRecreatorTestModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/ProfileChooserSpanTest.kt b/app/src/test/java/org/oppia/android/app/testing/ProfileChooserSpanTest.kt index 83f154ff62d..9d428db0559 100644 --- a/app/src/test/java/org/oppia/android/app/testing/ProfileChooserSpanTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/ProfileChooserSpanTest.kt @@ -32,13 +32,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -378,7 +381,8 @@ class ProfileChooserSpanTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/ProfileProgressSpanCount.kt b/app/src/test/java/org/oppia/android/app/testing/ProfileProgressSpanCount.kt index 247e6382b90..461919298a4 100644 --- a/app/src/test/java/org/oppia/android/app/testing/ProfileProgressSpanCount.kt +++ b/app/src/test/java/org/oppia/android/app/testing/ProfileProgressSpanCount.kt @@ -33,13 +33,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -162,7 +165,8 @@ class ProfileProgressSpanCount { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/RecentlyPlayedSpanTest.kt b/app/src/test/java/org/oppia/android/app/testing/RecentlyPlayedSpanTest.kt index 1f3325e4521..f6eeaa709ce 100644 --- a/app/src/test/java/org/oppia/android/app/testing/RecentlyPlayedSpanTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/RecentlyPlayedSpanTest.kt @@ -35,13 +35,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -297,7 +300,8 @@ class RecentlyPlayedSpanTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/TopicRevisionSpanTest.kt b/app/src/test/java/org/oppia/android/app/testing/TopicRevisionSpanTest.kt index c4e56b6316a..931f5c69356 100644 --- a/app/src/test/java/org/oppia/android/app/testing/TopicRevisionSpanTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/TopicRevisionSpanTest.kt @@ -32,13 +32,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -162,7 +165,8 @@ class TopicRevisionSpanTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/activity/TestActivityTest.kt b/app/src/test/java/org/oppia/android/app/testing/activity/TestActivityTest.kt index 915e262e512..b13424928ea 100644 --- a/app/src/test/java/org/oppia/android/app/testing/activity/TestActivityTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/activity/TestActivityTest.kt @@ -29,13 +29,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -187,7 +190,9 @@ class TestActivityTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, ActivityRecreatorTestModule::class, LocaleProdModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/administratorcontrols/AdministratorControlsFragmentTest.kt b/app/src/test/java/org/oppia/android/app/testing/administratorcontrols/AdministratorControlsFragmentTest.kt index 0984dbbae3b..842475f0213 100644 --- a/app/src/test/java/org/oppia/android/app/testing/administratorcontrols/AdministratorControlsFragmentTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/administratorcontrols/AdministratorControlsFragmentTest.kt @@ -43,13 +43,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -222,7 +225,9 @@ class AdministratorControlsFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt b/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt index 7b3b36d0d01..41d04fdc19e 100644 --- a/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt @@ -42,13 +42,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -271,7 +274,9 @@ class OptionsFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/player/split/PlayerSplitScreenTesting.kt b/app/src/test/java/org/oppia/android/app/testing/player/split/PlayerSplitScreenTesting.kt index db50ed5a66b..2750f397034 100644 --- a/app/src/test/java/org/oppia/android/app/testing/player/split/PlayerSplitScreenTesting.kt +++ b/app/src/test/java/org/oppia/android/app/testing/player/split/PlayerSplitScreenTesting.kt @@ -30,13 +30,16 @@ import org.oppia.android.app.utility.SplitScreenManager 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -190,7 +193,8 @@ class PlayerSplitScreenTesting { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/player/state/StateFragmentAccessibilityTest.kt b/app/src/test/java/org/oppia/android/app/testing/player/state/StateFragmentAccessibilityTest.kt index b198682e10c..6cc58786788 100644 --- a/app/src/test/java/org/oppia/android/app/testing/player/state/StateFragmentAccessibilityTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/player/state/StateFragmentAccessibilityTest.kt @@ -37,13 +37,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -204,7 +207,8 @@ class StateFragmentAccessibilityTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/topic/info/TopicInfoFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/topic/info/TopicInfoFragmentLocalTest.kt index 0d22b3a092d..131d7e63b57 100644 --- a/app/src/test/java/org/oppia/android/app/topic/info/TopicInfoFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/topic/info/TopicInfoFragmentLocalTest.kt @@ -29,13 +29,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -145,7 +148,9 @@ class TopicInfoFragmentLocalTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentLocalTest.kt index 534a0d63c58..51045169537 100644 --- a/app/src/test/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentLocalTest.kt @@ -28,13 +28,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -148,7 +151,9 @@ class TopicLessonsFragmentLocalTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt index 71f96f4660d..42f7fad317e 100644 --- a/app/src/test/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt @@ -45,13 +45,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -411,7 +414,9 @@ class QuestionPlayerActivityLocalTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityLocalTest.kt index eaa6ad78915..1cdc640a5ce 100644 --- a/app/src/test/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityLocalTest.kt @@ -28,13 +28,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -137,7 +140,9 @@ class RevisionCardActivityLocalTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt b/app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt index 9ae15728414..835f8d78d19 100644 --- a/app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt +++ b/app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt @@ -34,13 +34,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -512,7 +515,9 @@ class AppLanguageResourceHandlerTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleTestModule::class, ActivityRecreatorTestModule::class, - ActivityIntentFactoriesModule::class, PlatformParameterSingletonModule::class + ActivityIntentFactoriesModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/translation/AppLanguageWatcherMixinTest.kt b/app/src/test/java/org/oppia/android/app/translation/AppLanguageWatcherMixinTest.kt index b86d62f1f51..6c368dad4eb 100644 --- a/app/src/test/java/org/oppia/android/app/translation/AppLanguageWatcherMixinTest.kt +++ b/app/src/test/java/org/oppia/android/app/translation/AppLanguageWatcherMixinTest.kt @@ -39,13 +39,16 @@ import org.oppia.android.app.translation.testing.TestActivityRecreator 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -260,7 +263,9 @@ class AppLanguageWatcherMixinTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleTestModule::class, ActivityRecreatorTestModule::class, - ActivityIntentFactoriesModule::class, PlatformParameterSingletonModule::class + ActivityIntentFactoriesModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/utility/datetime/DateTimeUtilTest.kt b/app/src/test/java/org/oppia/android/app/utility/datetime/DateTimeUtilTest.kt index 3f4504a1045..6e92fd30212 100644 --- a/app/src/test/java/org/oppia/android/app/utility/datetime/DateTimeUtilTest.kt +++ b/app/src/test/java/org/oppia/android/app/utility/datetime/DateTimeUtilTest.kt @@ -30,13 +30,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -174,7 +177,9 @@ class DateTimeUtilTest { HtmlParserEntityTypeModule::class, NetworkConnectionDebugUtilModule::class, DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/InteractionsModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/InteractionsModule.kt index 4175bda19eb..3dec6a50142 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/InteractionsModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/InteractionsModule.kt @@ -4,13 +4,16 @@ import dagger.Module import dagger.Provides import dagger.multibindings.IntoMap import dagger.multibindings.StringKey +import org.oppia.android.domain.classify.rules.AlgebraicExpressionInputRules import org.oppia.android.domain.classify.rules.ContinueRules import org.oppia.android.domain.classify.rules.DragDropSortInputRules import org.oppia.android.domain.classify.rules.FractionInputRules import org.oppia.android.domain.classify.rules.ImageClickInputRules import org.oppia.android.domain.classify.rules.ItemSelectionInputRules +import org.oppia.android.domain.classify.rules.MathEquationInputRules import org.oppia.android.domain.classify.rules.MultipleChoiceInputRules import org.oppia.android.domain.classify.rules.NumberWithUnitsRules +import org.oppia.android.domain.classify.rules.NumericExpressionInputRules import org.oppia.android.domain.classify.rules.NumericInputRules import org.oppia.android.domain.classify.rules.RatioExpressionInputRules import org.oppia.android.domain.classify.rules.TextInputRules @@ -107,4 +110,32 @@ class InteractionsModule { ): InteractionClassifier { return GenericInteractionClassifier(ruleClassifiers) } + + @Provides + @IntoMap + @StringKey("NumericExpressionInput") + fun provideNumericExpressionInputInteractionClassifier( + @NumericExpressionInputRules ruleClassifiers: Map + ): InteractionClassifier { + return GenericInteractionClassifier(ruleClassifiers) + } + + @Provides + @IntoMap + @StringKey("AlgebraicExpressionInput") + fun provideAlgebraicExpressionInputInteractionClassifier( + @AlgebraicExpressionInputRules ruleClassifiers: + Map + ): InteractionClassifier { + return GenericInteractionClassifier(ruleClassifiers) + } + + @Provides + @IntoMap + @StringKey("MathEquationInput") + fun provideMathEquationInputInteractionClassifier( + @MathEquationInputRules ruleClassifiers: Map + ): InteractionClassifier { + return GenericInteractionClassifier(ruleClassifiers) + } } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/AnswerClassificationControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/AnswerClassificationControllerTest.kt index 2f682aee259..ff0fb19c488 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/AnswerClassificationControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/AnswerClassificationControllerTest.kt @@ -31,13 +31,16 @@ import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createReal import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createSetOfTranslatableHtmlContentIds import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createString import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createTranslatableSetOfNormalizedString +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -844,7 +847,9 @@ class AnswerClassificationControllerTest { ImageClickInputModule::class, RatioInputModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, LoggerModule::class, TestDispatcherModule::class, LogStorageModule::class, NetworkConnectionUtilDebugModule::class, - TestLogReportingModule::class, AssetModule::class, RobolectricModule::class + TestLogReportingModule::class, AssetModule::class, RobolectricModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent { diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt index 1c7d718ef49..361bedc9528 100644 --- a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt @@ -25,13 +25,16 @@ import org.oppia.android.app.model.EphemeralState import org.oppia.android.app.model.Exploration import org.oppia.android.app.model.ExplorationCheckpoint import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -351,7 +354,8 @@ class ExplorationDataControllerTest { RatioInputModule::class, RobolectricModule::class, FakeOppiaClockModule::class, TestExplorationStorageModule::class, HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, - AssetModule::class, LocaleProdModule::class + AssetModule::class, LocaleProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : DataProvidersInjector { diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt index a87412cf98b..337a0a98132 100644 --- a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt @@ -45,13 +45,16 @@ import org.oppia.android.app.model.TranslatableHtmlContentId import org.oppia.android.app.model.UserAnswer import org.oppia.android.app.model.WrittenTranslationLanguageSelection import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -3618,7 +3621,8 @@ class ExplorationProgressControllerTest { RatioInputModule::class, RobolectricModule::class, FakeOppiaClockModule::class, TestExplorationStorageModule::class, HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, - AssetModule::class, LocaleProdModule::class + AssetModule::class, LocaleProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : DataProvidersInjector { diff --git a/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt index c914d63f58e..5539f784c4f 100644 --- a/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt @@ -38,13 +38,16 @@ import org.oppia.android.app.model.UserAnswer import org.oppia.android.app.model.UserAssessmentPerformance import org.oppia.android.app.model.WrittenTranslationLanguageSelection import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -1832,7 +1835,8 @@ class QuestionAssessmentProgressControllerTest { RatioInputModule::class, RobolectricModule::class, FakeOppiaClockModule::class, CachingTestModule::class, HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, - AssetModule::class, LocaleProdModule::class + AssetModule::class, LocaleProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : DataProvidersInjector { diff --git a/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt index 5f592a50171..f116d87232d 100644 --- a/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt @@ -23,13 +23,16 @@ import org.mockito.junit.MockitoRule import org.oppia.android.app.model.EphemeralQuestion import org.oppia.android.app.model.ProfileId import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -329,7 +332,9 @@ class QuestionTrainingControllerTest { LogStorageModule::class, TestDispatcherModule::class, RatioInputModule::class, RobolectricModule::class, FakeOppiaClockModule::class, CachingTestModule::class, HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class, - NetworkConnectionUtilDebugModule::class, AssetModule::class, LocaleProdModule::class + NetworkConnectionUtilDebugModule::class, AssetModule::class, LocaleProdModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : DataProvidersInjector { diff --git a/instrumentation/src/java/org/oppia/android/instrumentation/application/TestApplicationComponent.kt b/instrumentation/src/java/org/oppia/android/instrumentation/application/TestApplicationComponent.kt index 3e4cc2d9011..498b1f36e04 100644 --- a/instrumentation/src/java/org/oppia/android/instrumentation/application/TestApplicationComponent.kt +++ b/instrumentation/src/java/org/oppia/android/instrumentation/application/TestApplicationComponent.kt @@ -16,13 +16,16 @@ import org.oppia.android.app.topic.PracticeTabModule import org.oppia.android.app.translation.ActivityRecreatorProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -86,6 +89,8 @@ import javax.inject.Singleton DeveloperOptionsModule::class, PlatformParameterSyncUpWorkerModule::class, NetworkConnectionUtilDebugModule::class, EndToEndTestNetworkConfigModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorProdModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class, // TODO(#59): Remove this 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. NetworkConnectionDebugUtilModule::class diff --git a/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleCustomContextTest.kt b/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleCustomContextTest.kt index cd6da6c4408..e434ea51251 100644 --- a/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleCustomContextTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleCustomContextTest.kt @@ -29,13 +29,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -253,7 +256,9 @@ class InitializeDefaultLocaleRuleCustomContextTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, ActivityRecreatorTestModule::class, LocaleProdModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleOmissionTest.kt b/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleOmissionTest.kt index bbade8b3c62..65c8900a1a0 100644 --- a/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleOmissionTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleOmissionTest.kt @@ -26,13 +26,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -129,7 +132,9 @@ class InitializeDefaultLocaleRuleOmissionTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, ActivityRecreatorTestModule::class, LocaleProdModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleTest.kt b/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleTest.kt index 437fcfc545d..6846fd3c69a 100644 --- a/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleTest.kt @@ -30,13 +30,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -133,7 +136,9 @@ class InitializeDefaultLocaleRuleTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, ActivityRecreatorTestModule::class, LocaleProdModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { From 10db2ce6cd65b93393ca57d101963cfab1f0f554 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 23:16:44 -0800 Subject: [PATCH 028/134] Add a11y string generation for math expressions. This is mostly copied from #2173. --- app/BUILD.bazel | 4 + .../android/app/utility/math/BUILD.bazel | 22 + .../math/MathExpressionAccessibilityUtil.kt | 196 +++++ .../MathExpressionAccessibilityUtilTest.kt | 724 ++++++++++++++++++ .../testing/math/MathEquationSubject.kt | 2 +- .../testing/math/MathExpressionSubject.kt | 2 +- 6 files changed, 948 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/org/oppia/android/app/utility/math/BUILD.bazel create mode 100644 app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt create mode 100644 app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt diff --git a/app/BUILD.bazel b/app/BUILD.bazel index 3e09ece2557..80d5ca54a6d 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -801,6 +801,7 @@ TEST_DEPS = [ ":test_deps", "//app/src/main/java/org/oppia/android/app/testing/activity:test_activity", "//app/src/main/java/org/oppia/android/app/translation/testing:test_module", + "//app/src/main/java/org/oppia/android/app/utility/math:math_expression_accessibility_util", "//domain", "//domain/src/main/java/org/oppia/android/domain/audio:audio_player_controller", "//domain/src/main/java/org/oppia/android/domain/onboarding/testing:fake_exploration_meta_data_retriever", @@ -813,6 +814,8 @@ TEST_DEPS = [ "//testing/src/main/java/org/oppia/android/testing/espresso:konfetti_view_matcher", "//testing/src/main/java/org/oppia/android/testing/espresso:text_input_action", "//testing/src/main/java/org/oppia/android/testing/junit:initialize_default_locale_rule", + "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", + "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", "//testing/src/main/java/org/oppia/android/testing/mockito", "//testing/src/main/java/org/oppia/android/testing/network", "//testing/src/main/java/org/oppia/android/testing/network:test_module", @@ -848,6 +851,7 @@ TEST_DEPS = [ "//utility/src/main/java/org/oppia/android/util/accessibility:test_module", "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module", "//utility/src/main/java/org/oppia/android/util/caching/testing:caching_test_module", + "//utility/src/main/java/org/oppia/android/util/math:parser", ] # App module tests. Note that all tests are assumed to be tests with resources (even though not all diff --git a/app/src/main/java/org/oppia/android/app/utility/math/BUILD.bazel b/app/src/main/java/org/oppia/android/app/utility/math/BUILD.bazel new file mode 100644 index 00000000000..5b5ee7cb433 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/utility/math/BUILD.bazel @@ -0,0 +1,22 @@ +""" +General purposes utilities corresponding to displaying math expressions & constructs. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +kt_android_library( + name = "math_expression_accessibility_util", + srcs = [ + "MathExpressionAccessibilityUtil.kt", + ], + visibility = ["//app:app_visibility"], + deps = [ + ":dagger", + "//model/src/main/proto:languages_java_proto_lite", + "//model/src/main/proto:math_java_proto_lite", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + ], +) + +dagger_rules() diff --git a/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt b/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt new file mode 100644 index 00000000000..c353b625048 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt @@ -0,0 +1,196 @@ +package org.oppia.android.app.utility.math + +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathFunctionCall.FunctionType +import org.oppia.android.app.model.MathFunctionCall.FunctionType.FUNCTION_UNSPECIFIED +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import org.oppia.android.app.model.OppiaLanguage +import org.oppia.android.app.model.OppiaLanguage.ARABIC +import org.oppia.android.app.model.OppiaLanguage.BRAZILIAN_PORTUGUESE +import org.oppia.android.app.model.OppiaLanguage.ENGLISH +import org.oppia.android.app.model.OppiaLanguage.HINDI +import org.oppia.android.app.model.OppiaLanguage.HINGLISH +import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED +import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE +import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED +import org.oppia.android.app.model.Real.RealTypeCase.INTEGER +import org.oppia.android.util.math.toPlainText +import java.text.NumberFormat +import java.util.Locale +import javax.inject.Inject +import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator + +class MathExpressionAccessibilityUtil @Inject constructor() { + fun convertToHumanReadableString( + equation: MathEquation, + language: OppiaLanguage, + divAsFraction: Boolean + ): String? { + return when (language) { + ENGLISH -> equation.toHumanReadableEnglishString(divAsFraction) + ARABIC, HINDI, HINGLISH, PORTUGUESE, BRAZILIAN_PORTUGUESE, LANGUAGE_UNSPECIFIED, + UNRECOGNIZED -> null + } + } + + fun convertToHumanReadableString( + expression: MathExpression, + language: OppiaLanguage, + divAsFraction: Boolean + ): String? { + return when (language) { + ENGLISH -> expression.toHumanReadableEnglishString(divAsFraction) + ARABIC, HINDI, HINGLISH, PORTUGUESE, BRAZILIAN_PORTUGUESE, LANGUAGE_UNSPECIFIED, + UNRECOGNIZED -> null + } + } + + private companion object { + // TODO: move these to the UI layer & have them utilize non-translatable strings. + private val numberFormat by lazy { NumberFormat.getNumberInstance(Locale.US) } + private val singularOrdinalNames = mapOf( + 1 to "oneth", + 2 to "half", + 3 to "third", + 4 to "fourth", + 5 to "fifth", + 6 to "sixth", + 7 to "seventh", + 8 to "eighth", + 9 to "ninth", + 10 to "tenth", + ) + private val pluralOrdinalNames = mapOf( + 1 to "oneths", + 2 to "halves", + 3 to "thirds", + 4 to "fourths", + 5 to "fifths", + 6 to "sixths", + 7 to "sevenths", + 8 to "eighths", + 9 to "ninths", + 10 to "tenths", + ) + + private fun MathEquation.toHumanReadableEnglishString(divAsFraction: Boolean): String? { + val lhsStr = leftSide.toHumanReadableEnglishString(divAsFraction) + val rhsStr = rightSide.toHumanReadableEnglishString(divAsFraction) + return if (lhsStr != null && rhsStr != null) "$lhsStr equals $rhsStr" else null + } + + private fun MathExpression.toHumanReadableEnglishString(divAsFraction: Boolean): String? { + // Reference: + // https://docs.google.com/document/d/1P-dldXQ08O-02ZRG978paiWOSz0dsvcKpDgiV_rKH_Y/view. + return when (expressionTypeCase) { + CONSTANT -> if (constant.realTypeCase == INTEGER) { + numberFormat.format(constant.integer.toLong()) + } else constant.toPlainText() + VARIABLE -> when (variable) { + "z" -> "zed" + "Z" -> "Zed" + else -> variable + } + BINARY_OPERATION -> { + val lhs = binaryOperation.leftOperand + val rhs = binaryOperation.rightOperand + val lhsStr = lhs.toHumanReadableEnglishString(divAsFraction) + val rhsStr = rhs.toHumanReadableEnglishString(divAsFraction) + if (lhsStr == null || rhsStr == null) return null + when (binaryOperation.operator) { + ADD -> "$lhsStr plus $rhsStr" + SUBTRACT -> "$lhsStr minus $rhsStr" + MULTIPLY -> { + if (binaryOperation.canBeReadAsImplicitMultiplication()) { + "$lhsStr $rhsStr" + } else "$lhsStr times $rhsStr" + } + DIVIDE -> { + if (divAsFraction && lhs.isConstantInteger() && rhs.isConstantInteger()) { + val numerator = lhs.constant.integer + val denominator = rhs.constant.integer + if (numerator in 0..10 && denominator in 1..10 && denominator >= numerator) { + val ordinalName = + if (numerator == 1) { + singularOrdinalNames.getValue(denominator) + } else pluralOrdinalNames.getValue(denominator) + "$numerator $ordinalName" + } else "$lhsStr over $rhsStr" + } else if (divAsFraction) { + "the fraction with numerator $lhsStr and denominator $rhsStr" + } else "$lhsStr divided by $rhsStr" + } + EXPONENTIATE -> "$lhsStr raised to the power of $rhsStr" + BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> null + } + } + UNARY_OPERATION -> { + val operandStr = unaryOperation.operand.toHumanReadableEnglishString(divAsFraction) + when (unaryOperation.operator) { + NEGATE -> operandStr?.let { "negative $it" } + POSITIVE -> operandStr?.let { "positive $it" } + UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> null + } + } + FUNCTION_CALL -> { + val argStr = functionCall.argument.toHumanReadableEnglishString(divAsFraction) + when (functionCall.functionType) { + SQUARE_ROOT -> argStr?.let { + if (functionCall.argument.isSingleTerm()) { + "square root of $it" + } else "start square root $it end square root" + } + FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> null + } + } + GROUP -> group.toHumanReadableEnglishString(divAsFraction)?.let { + if (isSingleTerm()) it else "open parenthesis $it close parenthesis" + } + EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathBinaryOperation.canBeReadAsImplicitMultiplication(): Boolean { + // Note that exponentiation is specialized since it's higher precedence than multiplication + // which means the graph won't look like "constant * variable" for polynomial terms like 2x^4 + // (which are cases the system should read using implicit multiplication, e.g. "two x raised + // to the power of 4"). + if (!isImplicit || !leftOperand.isConstant()) return false + return rightOperand.isVariable() || rightOperand.isExponentiation() + } + + private fun MathExpression.isConstantInteger(): Boolean = + expressionTypeCase == CONSTANT && constant.realTypeCase == INTEGER + + private fun MathExpression.isConstant(): Boolean = expressionTypeCase == CONSTANT + + private fun MathExpression.isVariable(): Boolean = expressionTypeCase == VARIABLE + + private fun MathExpression.isExponentiation(): Boolean = + expressionTypeCase == BINARY_OPERATION && binaryOperation.operator == EXPONENTIATE + + private fun MathExpression.isSingleTerm(): Boolean = when (expressionTypeCase) { + CONSTANT, VARIABLE, FUNCTION_CALL -> true + BINARY_OPERATION, UNARY_OPERATION -> false + GROUP -> group.isSingleTerm() + EXPRESSIONTYPE_NOT_SET, null -> false + } + } +} diff --git a/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt b/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt new file mode 100644 index 00000000000..6ada50c67ff --- /dev/null +++ b/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt @@ -0,0 +1,724 @@ +package org.oppia.android.app.utility.math + +import android.app.Application +import androidx.appcompat.app.AppCompatActivity +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.StringSubject +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import org.junit.Before +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.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.OppiaLanguage +import org.oppia.android.app.model.OppiaLanguage.ARABIC +import org.oppia.android.app.model.OppiaLanguage.BRAZILIAN_PORTUGUESE +import org.oppia.android.app.model.OppiaLanguage.ENGLISH +import org.oppia.android.app.model.OppiaLanguage.HINDI +import org.oppia.android.app.model.OppiaLanguage.HINGLISH +import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED +import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE +import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED +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.algebraicexpressioninput.AlgebraicExpressionInputModule +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.mathequationinput.MathEquationInputModule +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.numericexpressioninput.NumericExpressionInputModule +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.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.math.MathEquationSubject +import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat +import org.oppia.android.testing.math.MathExpressionSubject +import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat +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.EnableConsoleLog +import org.oppia.android.util.logging.EnableFileLog +import org.oppia.android.util.logging.GlobalLogLevel +import org.oppia.android.util.logging.LogLevel +import org.oppia.android.util.math.MathExpressionParser +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +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.Inject +import javax.inject.Singleton + +/** Tests for [MathExpressionAccessibilityUtil]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(application = MathExpressionAccessibilityUtilTest.TestApplication::class) +class MathExpressionAccessibilityUtilTest { + @Inject lateinit var util: MathExpressionAccessibilityUtil + + @Before + fun setUp() { + setUpTestApplicationComponent() + } + + @Test + fun testHumanReadableString() { + // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). + + val exp1 = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp1).forHumanReadable(ARABIC).doesNotConvertToString() + + assertThat(exp1).forHumanReadable(HINDI).doesNotConvertToString() + + assertThat(exp1).forHumanReadable(HINGLISH).doesNotConvertToString() + + assertThat(exp1).forHumanReadable(PORTUGUESE).doesNotConvertToString() + + assertThat(exp1).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + + assertThat(exp1).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + + assertThat(exp1).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + + val exp2 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp2).forHumanReadable(ARABIC).doesNotConvertToString() + + assertThat(exp2).forHumanReadable(HINDI).doesNotConvertToString() + + assertThat(exp2).forHumanReadable(HINGLISH).doesNotConvertToString() + + assertThat(exp2).forHumanReadable(PORTUGUESE).doesNotConvertToString() + + assertThat(exp2).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + + assertThat(exp2).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + + assertThat(exp2).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + + val eq1 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") + assertThat(eq1).forHumanReadable(ARABIC).doesNotConvertToString() + + assertThat(eq1).forHumanReadable(HINDI).doesNotConvertToString() + + assertThat(eq1).forHumanReadable(HINGLISH).doesNotConvertToString() + + assertThat(eq1).forHumanReadable(PORTUGUESE).doesNotConvertToString() + + assertThat(eq1).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + + assertThat(eq1).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + + assertThat(eq1).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + + // specific cases (from rules & other cases): + val exp3 = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp3).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + assertThat(exp3).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + + val exp49 = parseNumericExpressionSuccessfullyWithAllErrors("-1") + assertThat(exp49).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("negative 1") + + val exp50 = parseNumericExpressionSuccessfullyWithAllErrors("+1") + assertThat(exp50).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("positive 1") + + val exp4 = parseNumericExpressionSuccessfullyWithoutOptionalErrors("((1))") + assertThat(exp4).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + + val exp5 = parseNumericExpressionSuccessfullyWithAllErrors("1+2") + assertThat(exp5).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 plus 2") + + val exp6 = parseNumericExpressionSuccessfullyWithAllErrors("1-2") + assertThat(exp6).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 minus 2") + + val exp7 = parseNumericExpressionSuccessfullyWithAllErrors("1*2") + assertThat(exp7).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 times 2") + + val exp8 = parseNumericExpressionSuccessfullyWithAllErrors("1/2") + assertThat(exp8).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 divided by 2") + + val exp9 = parseNumericExpressionSuccessfullyWithAllErrors("1+(1-2)") + assertThat(exp9) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("1 plus open parenthesis 1 minus 2 close parenthesis") + + val exp10 = parseNumericExpressionSuccessfullyWithAllErrors("2^3") + assertThat(exp10) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("2 raised to the power of 3") + + val exp11 = parseNumericExpressionSuccessfullyWithAllErrors("2^(1+2)") + assertThat(exp11) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("2 raised to the power of open parenthesis 1 plus 2 close parenthesis") + + val exp12 = parseNumericExpressionSuccessfullyWithAllErrors("100000*2") + assertThat(exp12).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("100,000 times 2") + + val exp13 = parseNumericExpressionSuccessfullyWithAllErrors("sqrt(2)") + assertThat(exp13).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + + val exp14 = parseNumericExpressionSuccessfullyWithAllErrors("√2") + assertThat(exp14).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + + val exp15 = parseNumericExpressionSuccessfullyWithAllErrors("sqrt(1+2)") + assertThat(exp15) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("start square root 1 plus 2 end square root") + + val singularOrdinalNames = mapOf( + 1 to "oneth", + 2 to "half", + 3 to "third", + 4 to "fourth", + 5 to "fifth", + 6 to "sixth", + 7 to "seventh", + 8 to "eighth", + 9 to "ninth", + 10 to "tenth", + ) + val pluralOrdinalNames = mapOf( + 1 to "oneths", + 2 to "halves", + 3 to "thirds", + 4 to "fourths", + 5 to "fifths", + 6 to "sixths", + 7 to "sevenths", + 8 to "eighths", + 9 to "ninths", + 10 to "tenths", + ) + for (denominatorToCheck in 1..10) { + for (numeratorToCheck in 0..denominatorToCheck) { + val exp16 = + parseNumericExpressionSuccessfullyWithAllErrors("$numeratorToCheck/$denominatorToCheck") + + val ordinalName = + if (numeratorToCheck == 1) { + singularOrdinalNames.getValue(denominatorToCheck) + } else pluralOrdinalNames.getValue(denominatorToCheck) + assertThat(exp16) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("$numeratorToCheck $ordinalName") + } + } + + val exp17 = parseNumericExpressionSuccessfullyWithAllErrors("-1/3") + assertThat(exp17) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("negative 1 third") + + val exp18 = parseNumericExpressionSuccessfullyWithAllErrors("-2/3") + assertThat(exp18) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("negative 2 thirds") + + val exp19 = parseNumericExpressionSuccessfullyWithAllErrors("10/11") + assertThat(exp19) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("10 over 11") + + val exp20 = parseNumericExpressionSuccessfullyWithAllErrors("121/7986") + assertThat(exp20) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("121 over 7,986") + + val exp21 = parseNumericExpressionSuccessfullyWithAllErrors("8/7") + assertThat(exp21) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("8 over 7") + + val exp22 = parseNumericExpressionSuccessfullyWithAllErrors("-10/-30") + assertThat(exp22) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("negative the fraction with numerator 10 and denominator negative 30") + + val exp23 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1") + assertThat(exp23).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + + val exp24 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("((1))") + assertThat(exp24).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + + val exp25 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp25).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x") + + val exp26 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("((x))") + assertThat(exp26).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x") + + val exp51 = parseAlgebraicExpressionSuccessfullyWithAllErrors("-x") + assertThat(exp51).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("negative x") + + val exp52 = parseAlgebraicExpressionSuccessfullyWithAllErrors("+x") + assertThat(exp52).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("positive x") + + val exp27 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x") + assertThat(exp27).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 plus x") + + val exp28 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1-x") + assertThat(exp28).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 minus x") + + val exp29 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1*x") + assertThat(exp29).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 times x") + + val exp30 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1/x") + assertThat(exp30).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 divided by x") + + val exp31 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1/x") + assertThat(exp31) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("the fraction with numerator 1 and denominator x") + + val exp32 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(1-x)") + assertThat(exp32) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("1 plus open parenthesis 1 minus x close parenthesis") + + val exp33 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2x") + assertThat(exp33).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("2 x") + + val exp34 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy") + assertThat(exp34).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x times y") + + val exp35 = parseAlgebraicExpressionSuccessfullyWithAllErrors("z") + assertThat(exp35).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("zed") + + val exp36 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2xz") + assertThat(exp36).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("2 x times zed") + + val exp37 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2") + assertThat(exp37) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("x raised to the power of 2") + + val exp38 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("x^(1+x)") + assertThat(exp38) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("x raised to the power of open parenthesis 1 plus x close parenthesis") + + val exp39 = parseAlgebraicExpressionSuccessfullyWithAllErrors("100000*2") + assertThat(exp39).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("100,000 times 2") + + val exp40 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(2)") + assertThat(exp40).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + + val exp41 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(x)") + assertThat(exp41).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of x") + + val exp42 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√2") + assertThat(exp42).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + + val exp43 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√x") + assertThat(exp43).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of x") + + val exp44 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(1+2)") + assertThat(exp44) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("start square root 1 plus 2 end square root") + + val exp45 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(1+x)") + assertThat(exp45) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("start square root 1 plus x end square root") + + val exp46 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√(1+x)") + assertThat(exp46) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("start square root open parenthesis 1 plus x close parenthesis end square root") + + for (denominatorToCheck in 1..10) { + for (numeratorToCheck in 0..denominatorToCheck) { + val exp16 = + parseAlgebraicExpressionSuccessfullyWithAllErrors("$numeratorToCheck/$denominatorToCheck") + + val ordinalName = + if (numeratorToCheck == 1) { + singularOrdinalNames.getValue(denominatorToCheck) + } else pluralOrdinalNames.getValue(denominatorToCheck) + assertThat(exp16) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("$numeratorToCheck $ordinalName") + } + } + + val exp47 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1") + assertThat(exp47).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + + val exp48 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x(5-y)") + assertThat(exp48) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("x times open parenthesis 5 minus y close parenthesis") + + val eq2 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/y") + assertThat(eq2) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("x equals 1 divided by y") + + val eq3 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/2") + assertThat(eq3) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("x equals 1 divided by 2") + + val eq4 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/y") + assertThat(eq4) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("x equals the fraction with numerator 1 and denominator y") + + val eq5 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/2") + assertThat(eq5) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("x equals 1 half") + + // Tests from examples in the PRD + val eq6 = parseAlgebraicEquationSuccessfullyWithAllErrors("3x^2+4y=62") + assertThat(eq6) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("3 x raised to the power of 2 plus 4 y equals 62") + + val exp53 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x+6)/(x-4)") + assertThat(exp53) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo( + "the fraction with numerator open parenthesis x plus 6 close parenthesis and denominator" + + " open parenthesis x minus 4 close parenthesis" + ) + + val exp54 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("4*(x)^(2)+20x") + assertThat(exp54) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("4 times x raised to the power of 2 plus 20 x") + + val exp55 = parseAlgebraicExpressionSuccessfullyWithAllErrors("3+x-5") + assertThat(exp55).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("3 plus x minus 5") + + val exp56 = + parseAlgebraicExpressionSuccessfullyWithAllErrors( + "Z+A-Z", allowedVariables = listOf("A", "Z") + ) + assertThat(exp56).forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("Zed plus A minus Zed") + + val exp57 = + parseAlgebraicExpressionSuccessfullyWithAllErrors( + "6C-5A-1", allowedVariables = listOf("A", "C") + ) + assertThat(exp57) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("6 C minus 5 A minus 1") + + val exp58 = + parseAlgebraicExpressionSuccessfullyWithAllErrors( + "5*Z-w", allowedVariables = listOf("Z", "w") + ) + assertThat(exp58) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("5 times Zed minus w") + + val exp59 = + parseAlgebraicExpressionSuccessfullyWithAllErrors( + "L*S-3S+L", allowedVariables = listOf("L", "S") + ) + assertThat(exp59) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("L times S minus 3 S plus L") + + val exp60 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(2+6+3+4)") + assertThat(exp60) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("2 times open parenthesis 2 plus 6 plus 3 plus 4 close parenthesis") + + val exp61 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(64)") + assertThat(exp61) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("square root of 64") + + val exp62 = + parseAlgebraicExpressionSuccessfullyWithAllErrors( + "√(a+b)", allowedVariables = listOf("a", "b") + ) + assertThat(exp62) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("start square root open parenthesis a plus b close parenthesis end square root") + + val exp63 = parseAlgebraicExpressionSuccessfullyWithAllErrors("3*10^-5") + assertThat(exp63) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("3 times 10 raised to the power of negative 5") + + val exp64 = + parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors( + "((x+2y)+5*(a-2b)+z)", allowedVariables = listOf("x", "y", "a", "b", "z") + ) + assertThat(exp64) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo( + "open parenthesis open parenthesis x plus 2 y close parenthesis plus 5 times open" + + " parenthesis a minus 2 b close parenthesis plus zed close parenthesis" + ) + } + + private fun MathExpressionSubject.forHumanReadable( + language: OppiaLanguage + ): HumanReadableStringChecker { + return HumanReadableStringChecker(language) { divAsFraction -> + util.convertToHumanReadableString(actual, language, divAsFraction) + } + } + + private fun MathEquationSubject.forHumanReadable( + language: OppiaLanguage + ): HumanReadableStringChecker { + return HumanReadableStringChecker(language) { divAsFraction -> + util.convertToHumanReadableString(actual, language, divAsFraction) + } + } + + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) + } + + private class HumanReadableStringChecker( + private val language: OppiaLanguage, + private val maybeConvertToHumanReadableString: (Boolean) -> String? + ) { + fun convertsToStringThat(): StringSubject = + assertThat(convertToHumanReadableString(language, /* divAsFraction= */ false)) + + fun convertsWithFractionsToStringThat(): StringSubject = + assertThat(convertToHumanReadableString(language, /* divAsFraction= */ true)) + + fun doesNotConvertToString() { + assertWithMessage("Expected to not convert to: $language") + .that(maybeConvertToHumanReadableString(/* divAsFraction= */ false)) + .isNull() + } + + private fun convertToHumanReadableString( + language: OppiaLanguage, + divAsFraction: Boolean + ): String { + val readableString = maybeConvertToHumanReadableString(divAsFraction) + assertWithMessage("Expected to convert to: $language").that(readableString).isNotNull() + return checkNotNull(readableString) // Verified in the above assertion check. + } + } + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + // TODO(#59): Either isolate these to their own shared test module, or use the real logging + // module in tests to avoid needing to specify these settings for tests. + @EnableConsoleLog + @Provides + fun provideEnableConsoleLog(): Boolean = true + + @EnableFileLog + @Provides + fun provideEnableFileLog(): Boolean = false + + @GlobalLogLevel + @Provides + fun provideGlobalLogLevel(): LogLevel = LogLevel.VERBOSE + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, RobolectricModule::class, FakeOppiaClockModule::class, + TestLogReportingModule::class, TestDispatcherModule::class, ApplicationModule::class, + ApplicationStartupListenerModule::class, WorkManagerConfigurationModule::class, + ImageParsingModule::class, AccessibilityTestModule::class, PracticeTabModule::class, + GcsResourceModule::class, NetworkConnectionUtilDebugModule::class, LogStorageModule::class, + NetworkModule::class, PlatformParameterModule::class, HintsAndSolutionProdModule::class, + CachingTestModule::class, InteractionsModule::class, ExplorationStorageModule::class, + QuestionModule::class, NetworkConfigProdModule::class, ContinueModule::class, + FractionInputModule::class, ItemSelectionInputModule::class, MultipleChoiceInputModule::class, + NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, + DragDropSortInputModule::class, ImageClickInputModule::class, RatioInputModule::class, + HintsAndSolutionConfigModule::class, ExpirationMetaDataRetrieverModule::class, + GlideImageLoaderModule::class, PrimeTopicAssetsControllerModule::class, + HtmlParserEntityTypeModule::class, NetworkConnectionDebugUtilModule::class, + DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, AssetModule::class, + LocaleProdModule::class, ActivityRecreatorTestModule::class, + PlatformParameterSingletonModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class + ] + ) + interface TestApplicationComponent : ApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(test: MathExpressionAccessibilityUtilTest) + } + + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerMathExpressionAccessibilityUtilTest_TestApplicationComponent.builder() + .setApplication(this) + .build() + } + + fun inject(test: MathExpressionAccessibilityUtilTest) { + component.inject(test) + } + + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() + } + + override fun getApplicationInjector(): ApplicationInjector = component + } + + private companion object { + private fun parseNumericExpressionSuccessfullyWithAllErrors( + expression: String + ): MathExpression { + val result = parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) + return (result as MathParsingResult.Success).result + } + + private fun parseNumericExpressionSuccessfullyWithoutOptionalErrors( + expression: String + ): MathExpression { + val result = + parseNumericExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY + ) + return (result as MathParsingResult.Success).result + } + + private fun parseNumericExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode + ): MathParsingResult { + return MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) + } + + private fun parseAlgebraicExpressionSuccessfullyWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables + ) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables, errorCheckingMode + ) + } + + private fun parseAlgebraicEquationSuccessfullyWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathEquation { + val result = + MathExpressionParser.parseAlgebraicEquation( + expression, allowedVariables, + ErrorCheckingMode.ALL_ERRORS + ) + return (result as MathParsingResult.Success).result + } + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt index 373b1434b0e..3e4b44e7449 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt @@ -11,7 +11,7 @@ import org.oppia.android.util.math.toRawLatex class MathEquationSubject( metadata: FailureMetadata, - private val actual: MathEquation + val actual: MathEquation ) : LiteProtoSubject(metadata, actual) { fun hasLeftHandSideThat(): MathExpressionSubject = assertThat(actual.leftSide) diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt index eea079a7e4b..b8e816fb60e 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt @@ -25,7 +25,7 @@ import org.oppia.android.util.math.toRawLatex // See: https://kotlinlang.org/docs/type-safe-builders.html. class MathExpressionSubject( metadata: FailureMetadata, - private val actual: MathExpression + val actual: MathExpression ) : LiteProtoSubject(metadata, actual) { fun hasStructureThatMatches(init: ExpressionComparator.() -> Unit) { // TODO: maybe verify that all aspects are verified? From d5dd596639571e8f47bfd6659a7ba4b61bc8b431 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 23:52:17 -0800 Subject: [PATCH 029/134] Fix broken test post-refactor. --- .../NumericInputEqualsRuleClassifierProviderTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From aceddf8022a6c52efe73d5302fe4b5985b38b045 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 23:57:43 -0800 Subject: [PATCH 030/134] Add reasonable import for abs(). --- .../java/org/oppia/android/util/math/FractionExtensions.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index e229fd49e40..41ca8ff2643 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -1,6 +1,7 @@ package org.oppia.android.util.math import org.oppia.android.app.model.Fraction +import kotlin.math.abs import kotlin.math.absoluteValue /** Returns whether this fraction has a fractional component. */ @@ -210,7 +211,7 @@ fun Int.toWholeNumberFraction(): Fraction { val intValue = this return Fraction.newBuilder().apply { isNegative = intValue < 0 - wholeNumber = kotlin.math.abs(intValue) + wholeNumber = abs(intValue) numerator = 0 denominator = 1 }.build() From 312708c58a4845d55b1ea3000bd7ab25d7bd0035 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 7 Jan 2022 18:17:44 -0800 Subject: [PATCH 031/134] Fix equals errors for equations. This splits the current error into two: one for no equals being present (& adds it), and one for too many equals. This better supports the UI errors that need to be displayed to the user in these cases. --- .../android/util/math/MathExpressionParser.kt | 95 +++++++++++++++---- .../android/util/math/MathParsingError.kt | 10 +- .../util/math/MathExpressionParserTest.kt | 12 ++- 3 files changed, 91 insertions(+), 26 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt index 80b0acd49b3..ebebe862430 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -23,7 +23,6 @@ import org.oppia.android.app.model.Real import org.oppia.android.util.math.MathExpressionParser.ParseContext.AlgebraicExpressionContext import org.oppia.android.util.math.MathExpressionParser.ParseContext.NumericExpressionContext import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError -import org.oppia.android.util.math.MathParsingError.EquationHasWrongNumberOfEqualsError import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError import org.oppia.android.util.math.MathParsingError.ExponentIsVariableExpressionError import org.oppia.android.util.math.MathParsingError.ExponentTooLargeError @@ -63,6 +62,8 @@ import org.oppia.android.util.math.MathTokenizer.Companion.Token.RightParenthesi import org.oppia.android.util.math.MathTokenizer.Companion.Token.SquareRootSymbol import org.oppia.android.util.math.MathTokenizer.Companion.Token.VariableName import kotlin.math.absoluteValue +import org.oppia.android.util.math.MathParsingError.EquationHasTooManyEqualsError +import org.oppia.android.util.math.MathParsingError.EquationIsMissingEqualsError class MathExpressionParser private constructor(private val parseContext: ParseContext) { // TODO: @@ -96,7 +97,12 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo return EquationMissingLhsOrRhsError.toFailure() } - val lhsResult = parseGenericExpression().also { + val lhsResult = parseGenericExpression().maybeFail { + // An equals sign must be present. + if (!parseContext.hasNextTokenOfType()) { + EquationIsMissingEqualsError + } else null + }.also { parseContext.consumeTokenOfType() }.maybeFail { if (!parseContext.hasMoreTokens()) { @@ -146,9 +152,9 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo private fun parseGenericAddExpressionRhs(): MathParsingResult { // generic_add_expression_rhs = plus_operator , generic_mult_div_expression ; - return parseContext.consumeTokenOfType().maybeFail { + return parseContext.consumeTokenOfType().maybeFail { token -> if (!parseContext.hasMoreTokens()) { - NoVariableOrNumberAfterBinaryOperatorError(ADD) + NoVariableOrNumberAfterBinaryOperatorError(ADD, parseContext.extractSubexpression(token)) } else null }.flatMap { parseGenericMultDivExpression() @@ -157,9 +163,11 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo private fun parseGenericSubExpressionRhs(): MathParsingResult { // generic_sub_expression_rhs = minus_operator , generic_mult_div_expression ; - return parseContext.consumeTokenOfType().maybeFail { + return parseContext.consumeTokenOfType().maybeFail { token -> if (!parseContext.hasMoreTokens()) { - NoVariableOrNumberAfterBinaryOperatorError(SUBTRACT) + NoVariableOrNumberAfterBinaryOperatorError( + SUBTRACT, parseContext.extractSubexpression(token) + ) } else null }.flatMap { parseGenericMultDivExpression() @@ -209,9 +217,11 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo private fun parseGenericMultExpressionRhs(): MathParsingResult { // generic_mult_expression_rhs = multiplication_operator , generic_exp_expression ; - return parseContext.consumeTokenOfType().maybeFail { + return parseContext.consumeTokenOfType().maybeFail { token -> if (!parseContext.hasMoreTokens()) { - NoVariableOrNumberAfterBinaryOperatorError(MULTIPLY) + NoVariableOrNumberAfterBinaryOperatorError( + MULTIPLY, parseContext.extractSubexpression(token) + ) } else null }.flatMap { parseGenericExpExpression() @@ -220,9 +230,9 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo private fun parseGenericDivExpressionRhs(): MathParsingResult { // generic_div_expression_rhs = division_operator , generic_exp_expression ; - return parseContext.consumeTokenOfType().maybeFail { + return parseContext.consumeTokenOfType().maybeFail { token -> if (!parseContext.hasMoreTokens()) { - NoVariableOrNumberAfterBinaryOperatorError(DIVIDE) + NoVariableOrNumberAfterBinaryOperatorError(DIVIDE, parseContext.extractSubexpression(token)) } else null }.flatMap { parseGenericExpExpression() @@ -270,9 +280,11 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo operator = EXPONENTIATE, rhsResult = lhsResult.flatMap { parseContext.consumeTokenOfType() - }.maybeFail { + }.maybeFail { token -> if (!parseContext.hasMoreTokens()) { - NoVariableOrNumberAfterBinaryOperatorError(EXPONENTIATE) + NoVariableOrNumberAfterBinaryOperatorError( + EXPONENTIATE, parseContext.extractSubexpression(token) + ) } else null }.flatMap { parseGenericExpExpression() @@ -307,7 +319,8 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } nextToken is BinaryOperatorToken -> { NoVariableOrNumberBeforeBinaryOperatorError( - operator = nextToken.getBinaryOperator() + operator = nextToken.getBinaryOperator(), + operatorSymbol = parseContext.extractSubexpression(nextToken) ).toFailure() } else -> GenericError.toFailure() @@ -315,7 +328,7 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } is EqualsSymbol -> { if (parseContext is AlgebraicExpressionContext && parseContext.isPartOfEquation) { - EquationHasWrongNumberOfEqualsError.toFailure() + EquationHasTooManyEqualsError.toFailure() } else GenericError.toFailure() } is IncompleteFunctionName -> nextToken.toFailure() @@ -592,7 +605,7 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo is LeftParenthesisSymbol, is RightParenthesisSymbol -> UnbalancedParenthesesError is EqualsSymbol -> { if (parseContext is AlgebraicExpressionContext && parseContext.isPartOfEquation) { - EquationHasWrongNumberOfEqualsError + EquationHasTooManyEqualsError } else GenericError } is IncompleteFunctionName -> nextToken.toError() @@ -699,8 +712,11 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo fun parseNumericExpression( rawExpression: String, errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS - ): MathParsingResult = - createNumericParser(rawExpression, errorCheckingMode).parseGenericExpressionGrammar() + ): MathParsingResult { + return createNumericParser(rawExpression, errorCheckingMode) + .parseGenericExpressionGrammar() + .map { it.stripParseInfo() } + } fun parseAlgebraicExpression( rawExpression: String, @@ -709,7 +725,7 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo ): MathParsingResult { return createAlgebraicParser( rawExpression, isPartOfEquation = false, allowedVariables, errorCheckingMode - ).parseGenericExpressionGrammar() + ).parseGenericExpressionGrammar().map { it.stripParseInfo() } } fun parseAlgebraicEquation( @@ -719,7 +735,7 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo ): MathParsingResult { return createAlgebraicParser( rawExpression, isPartOfEquation = true, allowedVariables, errorCheckingMode - ).parseGenericEquationGrammar() + ).parseGenericEquationGrammar().map { it.stripParseInfo() } } private fun createNumericParser( @@ -1040,5 +1056,46 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> false } } + + private fun MathExpression.stripParseInfo(): MathExpression { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + toBuilder().apply { + binaryOperation = this@stripParseInfo.binaryOperation.toBuilder().apply { + leftOperand = this@stripParseInfo.binaryOperation.leftOperand.stripParseInfo() + rightOperand = this@stripParseInfo.binaryOperation.rightOperand.stripParseInfo() + }.build() + }.build() + } + UNARY_OPERATION -> { + toBuilder().apply { + unaryOperation = this@stripParseInfo.unaryOperation.toBuilder().apply { + operand = this@stripParseInfo.unaryOperation.operand.stripParseInfo() + }.build() + }.build() + } + FUNCTION_CALL -> { + toBuilder().apply { + functionCall = this@stripParseInfo.functionCall.toBuilder().apply { + argument = this@stripParseInfo.functionCall.argument.stripParseInfo() + }.build() + }.build() + } + GROUP -> { + toBuilder().apply { + group = this@stripParseInfo.group.stripParseInfo() + }.build() + } + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> this + }.toBuilder().apply { + parseStartIndex = 0 + parseEndIndex = 0 + }.build() + } + + private fun MathEquation.stripParseInfo(): MathEquation = toBuilder().apply { + leftSide = this@stripParseInfo.leftSide.stripParseInfo() + rightSide = this@stripParseInfo.rightSide.stripParseInfo() + }.build() } } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt index 44fd1debb4a..ec19c7d745b 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt @@ -36,11 +36,13 @@ sealed class MathParsingError { object SubsequentUnaryOperatorsError : MathParsingError() data class NoVariableOrNumberBeforeBinaryOperatorError( - val operator: MathBinaryOperation.Operator + val operator: MathBinaryOperation.Operator, + val operatorSymbol: String ) : MathParsingError() data class NoVariableOrNumberAfterBinaryOperatorError( - val operator: MathBinaryOperation.Operator + val operator: MathBinaryOperation.Operator, + val operatorSymbol: String ) : MathParsingError() object ExponentIsVariableExpressionError : MathParsingError() @@ -57,7 +59,9 @@ sealed class MathParsingError { data class DisabledVariablesInUseError(val variables: List) : MathParsingError() - object EquationHasWrongNumberOfEqualsError : MathParsingError() + object EquationIsMissingEqualsError: MathParsingError() + + object EquationHasTooManyEqualsError: MathParsingError() object EquationMissingLhsOrRhsError : MathParsingError() diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt index 3c50966b01e..88e0a46ac79 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -10,7 +10,8 @@ import org.oppia.android.app.model.MathExpression import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError -import org.oppia.android.util.math.MathParsingError.EquationHasWrongNumberOfEqualsError +import org.oppia.android.util.math.MathParsingError.EquationHasTooManyEqualsError +import org.oppia.android.util.math.MathParsingError.EquationIsMissingEqualsError import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError import org.oppia.android.util.math.MathParsingError.ExponentIsVariableExpressionError import org.oppia.android.util.math.MathParsingError.ExponentTooLargeError @@ -240,13 +241,16 @@ class MathExpressionParserTest { assertThat((failure56 as DisabledVariablesInUseError).variables).containsExactly("x", "y", "z") val failure30 = expectFailureWhenParsingAlgebraicEquation("x==2") - assertThat(failure30).isInstanceOf(EquationHasWrongNumberOfEqualsError::class.java) + assertThat(failure30).isInstanceOf(EquationHasTooManyEqualsError::class.java) val failure31 = expectFailureWhenParsingAlgebraicEquation("x=2=y") - assertThat(failure31).isInstanceOf(EquationHasWrongNumberOfEqualsError::class.java) + assertThat(failure31).isInstanceOf(EquationHasTooManyEqualsError::class.java) val failure32 = expectFailureWhenParsingAlgebraicEquation("x=2=") - assertThat(failure32).isInstanceOf(EquationHasWrongNumberOfEqualsError::class.java) + assertThat(failure32).isInstanceOf(EquationHasTooManyEqualsError::class.java) + + val failure59 = expectFailureWhenParsingAlgebraicEquation("x") + assertThat(failure59).isInstanceOf(EquationIsMissingEqualsError::class.java) val failure33 = expectFailureWhenParsingAlgebraicEquation("x=") assertThat(failure33).isInstanceOf(EquationMissingLhsOrRhsError::class.java) From 9826b11704e1e2e51ee6c549661ea0b4e9d27e84 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 7 Jan 2022 18:19:45 -0800 Subject: [PATCH 032/134] Fix rational^rational powers. This generifies the sqrt algorithm to support n-roots so that rationals raised by rationals can actually work & retain the rational (in cases where the root can actually be taken). --- .../oppia/android/util/math/RealExtensions.kt | 106 ++++++++++-------- 1 file changed, 60 insertions(+), 46 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 83605b05808..f1d3e12d9e2 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -1,5 +1,6 @@ package org.oppia.android.util.math +import kotlin.math.absoluteValue import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.Real import org.oppia.android.app.model.Real.RealTypeCase.INTEGER @@ -84,15 +85,9 @@ fun Real.pow(rhs: Real): Real { RATIONAL -> { // Left-hand side is Fraction. when (rhs.realTypeCase) { - RATIONAL -> recompute { - if (rhs.rational.isOnlyWholeNumber()) { - // The fraction can be retained. - it.setRational(rational.pow(rhs.rational.wholeNumber)) - } else { - // The fraction can't realistically be retained since it's being raised to an actual - // fraction, resulting in an irrational number. - it.setIrrational(rational.toDouble().pow(rhs.rational.toDouble())) - } + // Anything raised by a fraction is pow'd by the numerator and rooted by the denominator. + RATIONAL -> rhs.rational.toImproperForm().let { power -> + rational.pow(power.numerator).root(power.denominator, power.isNegative) } IRRATIONAL -> recompute { it.setIrrational(rational.pow(rhs.irrational)) } INTEGER -> recompute { it.setRational(rational.pow(rhs.integer)) } @@ -111,14 +106,12 @@ fun Real.pow(rhs: Real): Real { INTEGER -> { // Left-hand side is an integer. when (rhs.realTypeCase) { - RATIONAL -> { - if (rhs.rational.isOnlyWholeNumber()) { - // Whole number-only fractions are effectively just int^int. - integer.pow(rhs.rational.wholeNumber) - } else { - // Otherwise, raising by a fraction will result in an irrational number. - recompute { it.setIrrational(integer.toDouble().pow(rhs.rational.toDouble())) } - } + // An integer raised to a fraction can use the same approach as above (fraction raised to + // fraction) by treating the integer as a whole number fraction. + RATIONAL -> rhs.rational.toImproperForm().let { power -> + integer.toWholeNumberFraction() + .pow(power.numerator) + .root(power.denominator, power.isNegative) } IRRATIONAL -> recompute { it.setIrrational(integer.toDouble().pow(rhs.irrational)) } INTEGER -> integer.pow(rhs.integer) @@ -196,43 +189,60 @@ private fun Int.pow(exp: Int): Real { } } -private fun sqrt(fraction: Fraction): Real { - val improper = fraction.toImproperForm() +private fun sqrt(fraction: Fraction): Real = fraction.root(base = 2, invert = false) - // Attempt to take the root of the fraction's numerator & denominator. - val numeratorRoot = sqrt(improper.numerator) - val denominatorRoot = sqrt(improper.denominator) +private fun Fraction.root(base: Int, invert: Boolean): Real { + check(base > 1) { "Expected base of 2 or higher, not: $base" } - // If both values stayed as integers, the original fraction can be retained. Otherwise, the - // fraction must be evaluated by performing a division. - return Real.newBuilder().apply { - if (numeratorRoot.realTypeCase == denominatorRoot.realTypeCase && numeratorRoot.isInteger()) { - val rootedFraction = Fraction.newBuilder().apply { - isNegative = improper.isNegative - numerator = numeratorRoot.integer - denominator = denominatorRoot.integer + val adjustedFraction = toImproperForm() + val adjustedNum = + if (adjustedFraction.isNegative) -adjustedFraction.numerator else adjustedFraction.numerator + val adjustedDenom = adjustedFraction.denominator + val rootedNumerator = if (invert) root(adjustedDenom, base) else root(adjustedNum, base) + val rootedDenominator = if (invert) root(adjustedNum, base) else root(adjustedDenom, base) + return if (rootedNumerator.isInteger() && rootedDenominator.isInteger()) { + Real.newBuilder().apply { + rational = Fraction.newBuilder().apply { + isNegative = rootedNumerator.isNegative() || rootedDenominator.isNegative() + numerator = rootedNumerator.integer.absoluteValue + denominator = rootedDenominator.integer.absoluteValue }.build().toProperForm() - if (rootedFraction.isOnlyWholeNumber()) { - // If the fractional form doesn't need to be kept, remove it. - integer = rootedFraction.toWholeNumber() - } else { - rational = rootedFraction - } - } else { - irrational = numeratorRoot.toDouble() - } - }.build() + }.build() + } else { + // One or both of the components of the fraction can't be rooted, so compute an irrational + // version. + Real.newBuilder().apply { + irrational = rootedNumerator.toDouble() / rootedDenominator.toDouble() + }.build() + } } -private fun sqrt(int: Int): Real { - // First, check if the integer is a square. Reference for possible methods: +private fun sqrt(int: Int): Real = root(int, base = 2) + +private fun root(int: Int, base: Int): Real { + // First, check if the integer is a root. Base reference for possible methods: // https://www.researchgate.net/post/How-to-decide-if-a-given-number-will-have-integer-square-root-or-not. - var potentialRoot = 2 - while ((potentialRoot * potentialRoot) < int) { + check(base > 1) { "Expected base of 2 or higher, not: $base" } + check((int < 0 && base.isOdd()) || int >= 0) { "Radicand results in imaginary number: $int" } + + if (int == 1) { + // 1^x is always 1. + return Real.newBuilder().apply { + integer = 1 + }.build() + } + + val radicand = int.absoluteValue + var potentialRoot = base + while (potentialRoot.pow(base).integer < radicand) { potentialRoot++ } - if (potentialRoot * potentialRoot == int) { + if (potentialRoot.pow(base).integer == radicand) { // There's an exact integer representation of the root. + if (int < 0 && base.isOdd()) { + // Odd roots of negative numbers retain the negative. + potentialRoot = -potentialRoot + } return Real.newBuilder().apply { integer = potentialRoot }.build() @@ -240,10 +250,14 @@ private fun sqrt(int: Int): Real { // Otherwise, compute the irrational square root. return Real.newBuilder().apply { - irrational = kotlin.math.sqrt(int.toDouble()) + irrational = if (base == 2) { + kotlin.math.sqrt(int.toDouble()) + } else int.toDouble().pow(1.0 / base.toDouble()) }.build() } +private fun Int.isOdd() = this % 2 == 1 + private fun Real.recompute(transform: (Real.Builder) -> Real.Builder): Real { return transform(newBuilderForType()).build() } From 58206b39b3c27d876cdfeb1a939d78be4b7ee680 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 7 Jan 2022 18:22:50 -0800 Subject: [PATCH 033/134] Ensure rational terms reduce to ints. This ensures cases like 8/1 become just '8' coefficients rather than staying as an irrational (for simplification). --- .../math/ExpressionToPolynomialConverter.kt | 5 ++- .../android/util/math/PolynomialExtensions.kt | 31 ++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt index d16ac87fce1..15b9678626b 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt @@ -27,7 +27,10 @@ import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator class ExpressionToPolynomialConverter private constructor() { companion object { fun MathExpression.reduceToPolynomial(): Polynomial? = - replaceSquareRoots().reduceToPolynomialAux()?.removeUnnecessaryVariables()?.sort() + replaceSquareRoots().reduceToPolynomialAux() + ?.removeUnnecessaryVariables() + ?.simplifyRationals() + ?.sort() private fun MathExpression.replaceSquareRoots(): MathExpression { return when (expressionTypeCase) { diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index 74b1187b6e0..0f35926b63f 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -120,6 +120,18 @@ fun Polynomial.removeUnnecessaryVariables(): Polynomial { }.build().ensureAtLeastConstant() } +fun Polynomial.simplifyRationals(): Polynomial { + return Polynomial.newBuilder().apply { + addAllTerm( + this@simplifyRationals.termList.map { term -> + term.toBuilder().apply { + coefficient = term.coefficient.maybeSimplifyRationalToInteger() + }.build() + } + ) + }.build() +} + fun Polynomial.sort(): Polynomial = Polynomial.newBuilder().apply { addAllTerm(this@sort.termList.sortedWith(POLYNOMIAL_TERM_COMPARATOR)) }.build() @@ -305,7 +317,11 @@ private fun Term.pow(rational: Fraction): Term? { if (newVariablePowers.any { !it.isOnlyWholeNumber() }) return null return Term.newBuilder().apply { - coefficient = this@pow.coefficient + coefficient = this@pow.coefficient.pow( + Real.newBuilder().apply { + this.rational = rational + }.build() + ) addAllVariable( this@pow.variableList.zip(newVariablePowers).map { (variable, newPower) -> variable.toBuilder().apply { @@ -356,3 +372,16 @@ private fun List.toPowerMap(): Map { private fun Map.toVariableList(): List { return map { (name, power) -> Variable.newBuilder().setName(name).setPower(power).build() } } + +private fun Real.maybeSimplifyRationalToInteger(): Real = when (realTypeCase) { + Real.RealTypeCase.RATIONAL -> { + if (rational.isOnlyWholeNumber()) { + Real.newBuilder().apply { + integer = this@maybeSimplifyRationalToInteger.rational.toWholeNumber() + }.build() + } else this + } + // Nothing to do in these cases. + Real.RealTypeCase.IRRATIONAL, Real.RealTypeCase.INTEGER, Real.RealTypeCase.REALTYPE_NOT_SET, + null -> this +} From e355d4ae5ab153236f4a5c3769c1ee5f004b173f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 7 Jan 2022 18:51:20 -0800 Subject: [PATCH 034/134] Add partial equivalence checking for new rules. This ensures irrational constants don't fail to compare when computed due to rounding inconsistencies in FPUs. --- ...putIsEquivalentToRuleClassifierProvider.kt | 3 +- ...atchesExactlyWithRuleClassifierProvider.kt | 3 +- ...vialManipulationsRuleClassifierProvider.kt | 3 +- .../org/oppia/android/util/math/BUILD.bazel | 12 +++++ .../math/ComparableOperationListExtensions.kt | 52 +++++++++++++++++++ .../util/math/MathExpressionExtensions.kt | 29 +++++++++++ .../android/util/math/PolynomialExtensions.kt | 18 ++++++- .../oppia/android/util/math/RealExtensions.kt | 14 +++-- 8 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 utility/src/main/java/org/oppia/android/util/math/ComparableOperationListExtensions.kt diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt index ecf89663802..50094be6215 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -11,6 +11,7 @@ import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.toPolynomial import javax.inject.Inject +import org.oppia.android.util.math.approximatelyEquals class NumericExpressionInputIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -31,7 +32,7 @@ class NumericExpressionInputIsEquivalentToRuleClassifierProvider @Inject constru ): Boolean { val answerExpression = parsePolynomial(answer) ?: return false val inputExpression = parsePolynomial(input) ?: return false - return answerExpression == inputExpression + return answerExpression.approximatelyEquals(inputExpression) } private fun parsePolynomial(rawExpression: String): Polynomial? { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt index 9ce52a59519..fe58ff2dca8 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -10,6 +10,7 @@ import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import javax.inject.Inject +import org.oppia.android.util.math.approximatelyEquals class NumericExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -30,7 +31,7 @@ class NumericExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject con ): Boolean { val answerExpression = parseNumericExpression(answer) ?: return false val inputExpression = parseNumericExpression(input) ?: return false - return answerExpression == inputExpression + return answerExpression.approximatelyEquals(inputExpression) } private fun parseNumericExpression(rawExpression: String): MathExpression? { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index d1ea260e948..5e17c0c4173 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -11,6 +11,7 @@ import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.toComparableOperationList import javax.inject.Inject +import org.oppia.android.util.math.approximatelyEquals class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider @Inject constructor( @@ -32,7 +33,7 @@ class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvide ): Boolean { val answerExpression = parseComparableOperationList(answer) ?: return false val inputExpression = parseComparableOperationList(input) ?: return false - return answerExpression == inputExpression + return answerExpression.approximatelyEquals(inputExpression) } private fun parseComparableOperationList(rawExpression: String): ComparableOperationList? { 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 index 3bdacb733c2..1543a82a77d 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -10,6 +10,7 @@ android_library( "//:oppia_api_visibility", ], exports = [ + ":comparable_operation_list_extensions", ":comparator_extensions", ":float_extensions", ":fraction_extensions", @@ -71,6 +72,17 @@ kt_android_library( ], ) +kt_android_library( + name = "comparable_operation_list_extensions", + srcs = [ + "ComparableOperationListExtensions.kt", + ], + deps = [ + ":real_extensions", + "//model/src/main/proto:math_java_proto_lite", + ], +) + kt_android_library( name = "comparator_extensions", srcs = [ diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparableOperationListExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparableOperationListExtensions.kt new file mode 100644 index 00000000000..32af4ff4acb --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/ComparableOperationListExtensions.kt @@ -0,0 +1,52 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation +import org.oppia.android.app.model.ComparableOperationList.NonCommutativeOperation + +/** + * Returns whether this [ComparableOperationList] is approximately equal to another, that is, + * whether it exactly matches the other except for constants (which instead utilize + * [Real.approximatelyEquals]). + */ +fun ComparableOperationList.approximatelyEquals(other: ComparableOperationList): Boolean { + return rootOperation.approximatelyEquals(other.rootOperation) +} + +private fun ComparableOperation.approximatelyEquals(other: ComparableOperation): Boolean { + if (isNegated != other.isNegated) return false + if (isInverted != other.isInverted) return false + if (comparisonTypeCase != other.comparisonTypeCase) return false + return when (comparisonTypeCase) { + ComparableOperation.ComparisonTypeCase.COMMUTATIVE_ACCUMULATION -> + commutativeAccumulation.approximatelyEquals(other.commutativeAccumulation) + ComparableOperation.ComparisonTypeCase.NON_COMMUTATIVE_OPERATION -> + nonCommutativeOperation.approximatelyEquals(other.nonCommutativeOperation) + ComparableOperation.ComparisonTypeCase.CONSTANT_TERM -> + constantTerm.approximatelyEquals(other.constantTerm) + ComparableOperation.ComparisonTypeCase.VARIABLE_TERM -> variableTerm == other.variableTerm + ComparableOperation.ComparisonTypeCase.COMPARISONTYPE_NOT_SET, null -> true + } +} + +private fun CommutativeAccumulation.approximatelyEquals(other: CommutativeAccumulation): Boolean { + if (accumulationType != other.accumulationType) return false + if (combinedOperationsCount != other.combinedOperationsCount) return false + return combinedOperationsList.zip(other.combinedOperationsList).all { (first, second) -> + first.approximatelyEquals(second) + } +} + +private fun NonCommutativeOperation.approximatelyEquals(other: NonCommutativeOperation): Boolean { + if (operationTypeCase != other.operationTypeCase) return false + return when (operationTypeCase) { + NonCommutativeOperation.OperationTypeCase.EXPONENTIATION -> { + exponentiation.leftOperand.approximatelyEquals(other.exponentiation.leftOperand) + && exponentiation.rightOperand.approximatelyEquals(other.exponentiation.rightOperand) + } + NonCommutativeOperation.OperationTypeCase.SQUARE_ROOT -> + squareRoot.approximatelyEquals(other.squareRoot) + NonCommutativeOperation.OperationTypeCase.OPERATIONTYPE_NOT_SET, null -> true + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 39e59ce99bb..22c66b37776 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -28,6 +28,35 @@ fun MathExpression.toComparableOperationList(): ComparableOperationList = fun MathExpression.toPolynomial(): Polynomial? = stripGroups().reduceToPolynomial() +/** + * Returns whether this [MathExpression] approximately equals another, that is, that it fully + * matches in its AST representation but all constants are compared using + * [Real.approximatelyEquals]. Further, this does not check parser markers when considering + * equivalence. + */ +fun MathExpression.approximatelyEquals(other: MathExpression): Boolean { + if (expressionTypeCase != other.expressionTypeCase) return false + return when (expressionTypeCase) { + CONSTANT -> constant.approximatelyEquals(other.constant) + VARIABLE -> variable == other.variable + BINARY_OPERATION -> { + binaryOperation.operator == other.binaryOperation.operator + && binaryOperation.leftOperand.approximatelyEquals(other.binaryOperation.leftOperand) + && binaryOperation.rightOperand.approximatelyEquals(other.binaryOperation.rightOperand) + } + UNARY_OPERATION -> { + unaryOperation.operator == other.unaryOperation.operator + && unaryOperation.operand.approximatelyEquals(other.unaryOperation.operand) + } + FUNCTION_CALL -> { + functionCall.functionType == other.functionCall.functionType + && functionCall.argument.approximatelyEquals(other.functionCall.argument) + } + GROUP -> group.approximatelyEquals(other.group) + EXPRESSIONTYPE_NOT_SET, null -> true + } +} + private fun MathExpression.stripGroups(): MathExpression { return when (expressionTypeCase) { BINARY_OPERATION -> toBuilder().apply { diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index 0f35926b63f..33b00c26326 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -38,6 +38,22 @@ fun Polynomial.isSingleTerm(): Boolean = termList.size == 1 fun Polynomial.isApproximatelyZero(): Boolean = termList.all { it.coefficient.isApproximatelyZero() } // Zero polynomials only have 0 coefs. +/** + * Returns whether this [Polynomial] approximately equals an other, that is, that the polynomial has + * the exact same terms and approximately equal coefficients (see [Real.approximatelyEquals]). + */ +fun Polynomial.approximatelyEquals(other: Polynomial): Boolean { + if (termCount != other.termCount) return false + + // Terms can be zipped since they should be sorted prior to checking equivalence. + return termList.zip(other.termList).all { (first, second) -> first.approximatelyEquals(second) } +} + +private fun Term.approximatelyEquals(other: Term): Boolean { + // The variable lists can be exactly matched since they're sorted. + return coefficient.approximatelyEquals(other.coefficient) && variableList == other.variableList +} + /** * Returns the first term coefficient from this polynomial. This corresponds to the whole value of * the polynomial iff isConstant() returns true, otherwise this value isn't useful. @@ -65,7 +81,7 @@ private fun Term.toPlainText(): String { // Include the coefficient if there is one (coefficients of 1 are ignored only if there are // variables present). productValues += when { - variableList.isEmpty() || !abs(coefficient).isApproximatelyEqualTo(1.0) -> when { + variableList.isEmpty() || !abs(coefficient).approximatelyEquals(1.0) -> when { coefficient.isRational() && variableList.isNotEmpty() -> "(${coefficient.toPlainText()})" else -> coefficient.toPlainText() } diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 31bcf9702d4..6ff0552b88f 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -47,11 +47,19 @@ fun Real.isNegative(): Boolean = when (realTypeCase) { REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") } -fun Real.isApproximatelyEqualTo(value: Double): Boolean { - return toDouble().approximatelyEquals(value) +fun Real.isApproximatelyZero(): Boolean = approximatelyEquals(0.0) + +/** + * Returns whether this [Real] approximately equals another, that is, if they evaluate to + * approximately the same value (see [Double.approximatelyEquals]). + */ +fun Real.approximatelyEquals(other: Real): Boolean { + return approximatelyEquals(other.toDouble()) } -fun Real.isApproximatelyZero(): Boolean = isApproximatelyEqualTo(0.0) +fun Real.approximatelyEquals(value: Double): Boolean { + return toDouble().approximatelyEquals(value) +} fun Real.toDouble(): Double { return when (realTypeCase) { From b073b4419d6d591b02ef7aaa5acad9ead2db8bd5 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 7 Jan 2022 18:53:00 -0800 Subject: [PATCH 035/134] Add partial constant checking for new rules. --- ...braicExpressionInputIsEquivalentToRuleClassifierProvider.kt | 3 ++- ...cExpressionInputMatchesExactlyWithRuleClassifierProvider.kt | 3 ++- ...putMatchesUpToTrivialManipulationsRuleClassifierProvider.kt | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt index 276e9de2868..a6ad8e8e010 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -11,6 +11,7 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingRes import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression import org.oppia.android.util.math.toPolynomial import javax.inject.Inject +import org.oppia.android.util.math.approximatelyEquals class AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -32,7 +33,7 @@ class AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider @Inject const val allowedVariables = classificationContext.extractAllowedVariables() val answerExpression = parsePolynomial(answer, allowedVariables) ?: return false val inputExpression = parsePolynomial(input, allowedVariables) ?: return false - return answerExpression == inputExpression + return answerExpression.approximatelyEquals(inputExpression) } private fun parsePolynomial(rawExpression: String, allowedVariables: List): Polynomial? { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt index b23d434d2d6..6feeba0696a 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -10,6 +10,7 @@ import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression import javax.inject.Inject +import org.oppia.android.util.math.approximatelyEquals class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -31,7 +32,7 @@ class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject c val allowedVariables = classificationContext.extractAllowedVariables() val answerExpression = parseExpression(answer, allowedVariables) ?: return false val inputExpression = parseExpression(input, allowedVariables) ?: return false - return answerExpression == inputExpression + return answerExpression.approximatelyEquals(inputExpression) } private fun parseExpression( diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index a7062556901..03c39a3c64f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -11,6 +11,7 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingRes import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression import org.oppia.android.util.math.toComparableOperationList import javax.inject.Inject +import org.oppia.android.util.math.approximatelyEquals class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider @Inject constructor( @@ -33,7 +34,7 @@ class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvi val allowedVariables = classificationContext.extractAllowedVariables() val answerExpression = parseComparableOperationList(answer, allowedVariables) ?: return false val inputExpression = parseComparableOperationList(input, allowedVariables) ?: return false - return answerExpression == inputExpression + return answerExpression.approximatelyEquals(inputExpression) } private fun parseComparableOperationList( From 6a2337f65d655d5a0bf6aaabeff50858790245bb Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 7 Jan 2022 19:00:15 -0800 Subject: [PATCH 036/134] Add partial equivalence checking for new rules. --- ...thEquationInputIsEquivalentToRuleClassifierProvider.kt | 5 +++-- ...uationInputMatchesExactlyWithRuleClassifierProvider.kt | 8 +++++++- ...tchesUpToTrivialManipulationsRuleClassifierProvider.kt | 3 ++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt index 07de228a104..62552bb92b9 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt @@ -11,6 +11,7 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingRes import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation import org.oppia.android.util.math.toPolynomial import javax.inject.Inject +import org.oppia.android.util.math.approximatelyEquals class MathEquationInputIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -34,8 +35,8 @@ class MathEquationInputIsEquivalentToRuleClassifierProvider @Inject constructor( val (inputLhs, inputRhs) = parsePolynomials(input, allowedVariables) ?: return false // Sides may cross-match (i.e. it's fine to reorder around the '='). - return (answerLhs == inputLhs && answerRhs == inputRhs) || - (answerLhs == inputRhs && answerRhs == inputLhs) + return (answerLhs.approximatelyEquals(inputLhs) && answerRhs.approximatelyEquals(inputRhs)) || + (answerLhs.approximatelyEquals(inputRhs) && answerRhs.approximatelyEquals(inputLhs)) } private fun parsePolynomials( diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt index 3bb7e3d875c..8c9e79a9e9f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt @@ -10,6 +10,7 @@ import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation import javax.inject.Inject +import org.oppia.android.util.math.approximatelyEquals class MathEquationInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -31,7 +32,7 @@ class MathEquationInputMatchesExactlyWithRuleClassifierProvider @Inject construc val allowedVariables = classificationContext.extractAllowedVariables() val answerEquation = parseEquation(answer, allowedVariables) ?: return false val inputEquation = parseEquation(input, allowedVariables) ?: return false - return answerEquation == inputEquation + return answerEquation.approximatelyEquals(inputEquation) } private fun parseEquation( @@ -59,5 +60,10 @@ class MathEquationInputMatchesExactlyWithRuleClassifierProvider @Inject construc ?.map { it.normalizedString } ?: listOf() } + + private fun MathEquation.approximatelyEquals(other: MathEquation): Boolean { + return leftSide.approximatelyEquals(other.leftSide) + && rightSide.approximatelyEquals(other.rightSide) + } } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index 64b5e6215f6..690e5f3646b 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -11,6 +11,7 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingRes import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation import org.oppia.android.util.math.toComparableOperationList import javax.inject.Inject +import org.oppia.android.util.math.approximatelyEquals class MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider @Inject constructor( @@ -35,7 +36,7 @@ class MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider val (inputLhs, inputRhs) = parseComparableLists(input, allowedVariables) ?: return false // Sides must match (reordering around the '=' is not allowed by this classifier). - return answerLhs == inputLhs && answerRhs == inputRhs + return answerLhs.approximatelyEquals(inputLhs) && answerRhs.approximatelyEquals(inputRhs) } private fun parseComparableLists( From 7740c8111c6c0d6d50cf5842f32fdec3c2d4b4a0 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 13 Jan 2022 14:03:43 -0800 Subject: [PATCH 037/134] Post-merge fix. --- .../src/main/java/org/oppia/android/domain/profile/BUILD.bazel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/src/main/java/org/oppia/android/domain/profile/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/profile/BUILD.bazel index 606a6311ed7..7475ee9bcbe 100644 --- a/domain/src/main/java/org/oppia/android/domain/profile/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/profile/BUILD.bazel @@ -15,7 +15,7 @@ kt_android_library( "//data/src/main/java/org/oppia/android/data/persistence:cache_store", "//domain/src/main/java/org/oppia/android/domain/oppialogger:oppia_logger", "//domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions:controller", - "//model:profile_java_proto_lite", + "//model/src/main/proto:profile_java_proto_lite", "//third_party:javax_inject_javax_inject", "//utility/src/main/java/org/oppia/android/util/data:async_result", "//utility/src/main/java/org/oppia/android/util/data:data_provider", From 929de4f094889cc92b645edbd1d39fbadef64b4b Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 13 Jan 2022 16:12:30 -0800 Subject: [PATCH 038/134] Add regex check, docs, and resolve TODOs. This also changes regex handling in the check to be more generic for better flexibility when matching files. --- model/BUILD.bazel | 3 +- model/oppia_proto_library.bzl | 22 +++-- .../file_content_validation_checks.textproto | 81 ++++++++++--------- .../oppia/android/scripts/proto/BUILD.bazel | 18 ++--- .../regex/RegexPatternValidationCheck.kt | 8 +- .../regex/RegexPatternValidationCheckTest.kt | 43 ++++++++++ 6 files changed, 116 insertions(+), 59 deletions(-) diff --git a/model/BUILD.bazel b/model/BUILD.bazel index 1755f53361e..a2bc21dd309 100644 --- a/model/BUILD.bazel +++ b/model/BUILD.bazel @@ -1,3 +1,4 @@ """ -TODO: add docs +Temporary package for defining all protos used in the codebase. Eventually, these will be moved to +more targeted data directories post-Gradle. """ diff --git a/model/oppia_proto_library.bzl b/model/oppia_proto_library.bzl index 8f6ac753135..fbd0e3db5f9 100644 --- a/model/oppia_proto_library.bzl +++ b/model/oppia_proto_library.bzl @@ -1,19 +1,25 @@ """ -TODO: add docs +Bazel macros for defining proto libraries. """ load("@rules_proto//proto:defs.bzl", "proto_library") -# TODO: add regex check -# TODO: add TODO to remove -# TODO: maybe close format proto issue with this PR? - -def oppia_proto_library(name, strip_import_prefix = "", **kwargs): +# TODO(#4096): Remove this once it's no longer needed. +def oppia_proto_library(name, **kwargs): """ - TODO: add docs + Defines a new proto library. + + Note that the library is defined with a stripped import prefix which ensures that protos have a + common import directory (which is needed since Gradle builds protos in the same directory + whereas Bazel doesn't by default). This common import directory is needed for cross-proto + textprotos to work correctly. + + Args: + name: str. The name of the proto library. + **kwargs: additional parameters to pass into proto_library. """ proto_library( name = name, - strip_import_prefix = strip_import_prefix, + strip_import_prefix = "", **kwargs ) diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index 17f2abb6059..5bef515e6df 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -1,16 +1,16 @@ file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "^import .+?support.+?$" failure_message: "AndroidX should be used instead of the support library" } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "CoroutineWorker" failure_message: "For stable tests, prefer using ListenableWorker with an Oppia-managed dispatcher." exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "SettableFuture" failure_message: "SettableFuture should only be used in pre-approved locations since it's easy to potentially mess up & lead to a hanging ListenableFuture." exempted_file_name: "domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorker.kt" @@ -18,90 +18,90 @@ file_content_checks { exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "android:gravity=\"left\"" failure_message: "Use android:gravity=\"start\", instead, for proper RTL support" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "android:gravity=\"right\"" failure_message: "Use android:gravity=\"end\", instead, for proper RTL support" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "android:layout_gravity=\"left\"" failure_message: "Use android:layout_gravity=\"start\", instead, for proper RTL support" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "android:layout_gravity=\"right\"" failure_message: "Use android:layout_gravity=\"end\", instead, for proper RTL support" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "paddingLeft|paddingRight|drawableLeft|drawableRight|layout_alignLeft|layout_alignRight|layout_marginLeft|layout_marginRight|layout_alignParentLeft|layout_alignParentRight|layout_toLeftOf|layout_toRightOf|layout_constraintLeft_toLeftOf|layout_constraintLeft_toRightOf|layout_constraintRight_toLeftOf|layout_constraintRight_toRightOf|layout_goneMarginLeft|layout_goneMarginRight" failure_message: "Use start/end versions of layout properties, instead, for proper RTL support" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "app:barrierDirection=\"left\"" failure_message: "Use app:barrierDirection=\"start\", instead, for proper RTL support" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "app:barrierDirection=\"right\"" failure_message: "Use app:barrierDirection=\"end\", instead, for proper RTL support" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "motion:dragDirection=\"left\"" failure_message: "Use motion:dragDirection=\"start\", instead, for proper RTL support" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "motion:dragDirection=\"right\"" failure_message: "Use motion:dragDirection=\"end\", instead, for proper RTL support" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "motion:touchAnchorSide=\"left\"" failure_message: "Use motion:touchAnchorSide=\"start\", instead, for proper RTL support" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "motion:touchAnchorSide=\"right\"" failure_message: "Use motion:touchAnchorSide=\"end\", instead, for proper RTL support" } file_content_checks { - file_path_regex: "app/src/main/res/values/strings.xml" + file_path_regex: "app/src/main/res/values/strings\\.xml" prohibited_content_regex: "Oppia" failure_message: "Oppia should never used directly in a string (since it shouldn't be translated). Instead, use a parameter & insert the string retrieved from app_name." } file_content_checks { - file_path_regex: "app/src/main/res/values/strings.xml" + file_path_regex: "app/src/main/res/values/strings\\.xml" prohibited_content_regex: "translatable=\"false\"" failure_message: "Untranslatable strings should go in untranslated_strings.xml, instead." } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "" failure_message: "All strings outside strings.xml must be marked as not translatable, or moved to strings.xml." exempted_file_patterns: "app/src/main/res/values.*?/strings\\.xml" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "" failure_message: "All plurals outside strings.xml must be marked as not translatable, or moved to strings.xml." exempted_file_patterns: "app/src/main/res/values.*?/strings\\.xml" } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "android.text.BidiFormatter" failure_message: "Do not use Android's BidiFormatter directly. Instead, use AndroidX's BidiFormatter for KitKat compatibility." exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "androidx.core.text.BidiFormatter" failure_message: "Do not use AndroidX's BidiFormatter directly. Instead, use the wrapper utility OppiaBidiFormatter so that tests can verify that formatting actually occurs on select strings." exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" @@ -110,7 +110,7 @@ file_content_checks { exempted_file_name: "utility/src/main/java/org/oppia/android/util/locale/OppiaBidiFormatterImpl.kt" } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "(format|getString|getStringArray|getQuantityString|getQuantityText|toLowerCase|toUpperCase|capitalize|decapitalize|lowercase|uppercase)\\(" failure_message: "String formatting and resource retrieval should go through AppLanguageResourceHandler, OppiaLocale.DisplayLocale, or OppiaLocale.MachineLocale depending on the context (see each class's documentation for details on when each should be used)." exempted_file_name: "domain/src/main/java/org/oppia/android/domain/locale/DisplayLocaleImpl.kt" @@ -128,29 +128,29 @@ file_content_checks { exempted_file_patterns: "scripts/.+" } file_content_checks { - file_path_regex: ".+?.java" + file_path_regex: ".+?\\.java" prohibited_content_regex: "(format|getString|getStringArray|getQuantityString|getQuantityText|toLowerCase|toUpperCase)\\(" failure_message: "String formatting and resource retrieval should go through AppLanguageResourceHandler, OppiaLocale.DisplayLocale, or OppiaLocale.MachineLocale depending on the context (see each class's documentation for details on when each should be used)." } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "ignoreCase\\s*?=" failure_message: "Case-insensitive string operations should be performed using MachineLocale." - exempted_file_patterns: "testing/src/main/.+?.kt" + exempted_file_patterns: "testing/src/main/.+?\\.kt" exempted_file_patterns: "scripts/.+" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "(format|getString|getStringArray)\\(" failure_message: "String formatting and resource retrieval in layouts should go through AppLanguageResourceHandler." } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "@string/[^\\s]+?\\(" failure_message: "String formatting and quantity string building shouldn't be done directly through databinding. Instead, pass in AppLanguageResourceHandler from the view model or call a new function through the view model to compute the string. Both should use the handler's locale-safe formatting/quantity string methods." } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "@plurals/[^\\s]+?\\(" failure_message: "String plurals shouldn't be constructed directly through databinding. Instead, pass in AppLanguageResourceHandler from the view model or call a new function through the view model to compute the string. Both should use the handler's locale-safe formatting/quantity string methods." } @@ -160,13 +160,13 @@ file_content_checks { failure_message: "Only string type specifiers should use for strings (to avoid runtime errors due to bidirectional wrapping requirements)." } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "\\sActivity\\(" failure_message: "Activity should never be subclassed. Use AppCompatActivity, instead." exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "\\sAppCompatActivity\\(" failure_message: "Never subclass AppCompatActivity directly. Instead, use InjectableAppCompatActivity." exempted_file_name: "app/src/main/java/org/oppia/android/app/activity/InjectableAppCompatActivity.kt" @@ -176,30 +176,30 @@ file_content_checks { exempted_file_name: "testing/src/main/java/org/oppia/android/testing/TextInputActionTestActivity.kt" } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "\\sDialogFragment\\(" failure_message: "DialogFragment should never be subclassed. Use InjectableDialogFragment, instead." exempted_file_name: "app/src/main/java/org/oppia/android/app/fragment/InjectableDialogFragment.kt" exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" } file_content_checks { - file_path_regex: ".+?AndroidManifest.xml" + file_path_regex: ".+?AndroidManifest\\.xml" prohibited_content_regex: "android:configChanges" failure_message: "Never explicitly handle configuration changes. Instead, use saved instance states for retaining state across rotations. For other types of configuration changes, follow up with the developer mailing list with how to proceed if you think this is a legitimate case." } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "(android:drawableStart)|(android:drawableEnd)|(android:drawableTop)|(android:drawableBottom)|(android:src)" failure_message: "Drawable start/end/top/bottom & image source should use the compat versions, instead, e.g.: app:drawableStartCompat or app:srcCompat, to ensure that vector drawables can load properly in SDK <21 environments." } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "java.util.Optional" failure_message: "Prefer using com.google.common.base.Optional (Guava's Optional) since desugaring has some incompatibilities between Bazel & KitKat builds." exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "java\\.util\\.Calendar" failure_message: "Don't use Calendar directly. Instead, use OppiaClock and/or OppiaLocale for calendar-specific operations." exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" @@ -209,7 +209,7 @@ file_content_checks { exempted_file_name: "utility/src/main/java/org/oppia/android/util/system/OppiaClock.kt" } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "java\\.util\\.Date" failure_message: "Don't use Date directly. Instead, perform date-based operations using OppiaLocale." exempted_file_name: "app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt" @@ -221,7 +221,7 @@ file_content_checks { exempted_file_name: "utility/src/main/java/org/oppia/android/util/locale/MachineLocaleImpl.kt" } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "java\\.text" failure_message: "Don't perform date/time formatting directly. Instead, use OppiaLocale." exempted_file_name: "app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt" @@ -232,7 +232,7 @@ file_content_checks { exempted_file_name: "utility/src/main/java/org/oppia/android/util/locale/MachineLocaleImpl.kt" } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "java\\.util\\.Locale" failure_message: "Don't use Locale directly. Instead, use LocaleController, or OppiaLocale & its subclasses." exempted_file_name: "app/src/main/java/org/oppia/android/app/player/audio/LanguageDialogFragment.kt" @@ -252,8 +252,13 @@ file_content_checks { exempted_file_patterns: "utility/src/(?:((main)|(test)))/java/org/oppia/android/util/locale/.+?\\.kt" } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "kotlin\\.properties\\.Delegates" failure_message: "Don't use Delegates; use a lateinit var or nullable primitive var default-initialized to null, instead. Delegates uses reflection internally, have a non-trivial initialization cost, and can cause breakages on KitKat devices. See #3939 for more context." exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" } +file_content_checks { + file_path_regex: "BUILD" + prohibited_content_regex: "^proto_library\\(" + failure_message: "Don't use proto_library. Use oppia_proto_library instead." +} diff --git a/scripts/src/java/org/oppia/android/scripts/proto/BUILD.bazel b/scripts/src/java/org/oppia/android/scripts/proto/BUILD.bazel index b8ea5902201..60f6588f1a0 100644 --- a/scripts/src/java/org/oppia/android/scripts/proto/BUILD.bazel +++ b/scripts/src/java/org/oppia/android/scripts/proto/BUILD.bazel @@ -1,15 +1,15 @@ """ This library contains all protos used in the scripts module. -In Bazel, proto files are built using the proto_library() and java_lite_proto_library() rules. -The proto_library() rule creates a proto file library to be used in multiple languages. +In Bazel, proto files are built using the oppia_proto_library() and java_lite_proto_library() rules. +The oppia_proto_library() rule creates a proto file library to be used in multiple languages. The java_lite_proto_library() rule takes in a proto_library target and generates java code. -For more context on adding a new proto library, please refer to model/BUILD.bazel +For more context on adding a new proto library, please refer to model/BUILD """ load("@rules_java//java:defs.bzl", "java_lite_proto_library", "java_proto_library") -load("@rules_proto//proto:defs.bzl", "proto_library") +load("//model:oppia_proto_library.bzl", "oppia_proto_library") -proto_library( +oppia_proto_library( name = "affected_tests_proto", srcs = ["affected_tests.proto"], ) @@ -20,7 +20,7 @@ java_lite_proto_library( deps = [":affected_tests_proto"], ) -proto_library( +oppia_proto_library( name = "filename_pattern_validation_checks_proto", srcs = ["filename_pattern_validation_checks.proto"], visibility = ["//scripts:oppia_script_binary_visibility"], @@ -32,7 +32,7 @@ java_lite_proto_library( deps = [":filename_pattern_validation_checks_proto"], ) -proto_library( +oppia_proto_library( name = "file_content_validation_checks_proto", srcs = ["file_content_validation_checks.proto"], visibility = ["//scripts:oppia_script_binary_visibility"], @@ -44,7 +44,7 @@ java_lite_proto_library( deps = [":file_content_validation_checks_proto"], ) -proto_library( +oppia_proto_library( name = "script_exemptions_proto", srcs = ["script_exemptions.proto"], visibility = ["//scripts:oppia_script_binary_visibility"], @@ -56,7 +56,7 @@ java_lite_proto_library( deps = [":script_exemptions_proto"], ) -proto_library( +oppia_proto_library( name = "maven_dependencies_proto", srcs = ["maven_dependencies.proto"], visibility = ["//scripts:oppia_script_binary_visibility"], diff --git a/scripts/src/java/org/oppia/android/scripts/regex/RegexPatternValidationCheck.kt b/scripts/src/java/org/oppia/android/scripts/regex/RegexPatternValidationCheck.kt index 464251b2689..a910ac65a4a 100644 --- a/scripts/src/java/org/oppia/android/scripts/regex/RegexPatternValidationCheck.kt +++ b/scripts/src/java/org/oppia/android/scripts/regex/RegexPatternValidationCheck.kt @@ -218,7 +218,7 @@ private data class MatchableFileContentCheck( * (i.e. that it matches the inclusion pattern and is not explicitly or implicitly excluded). */ fun isFileAffectedByCheck(relativePath: String): Boolean = - filePathRegex.matches(relativePath) && !isFileExempted(relativePath) + filePathRegex.containsMatchIn(relativePath) && !isFileExempted(relativePath) /** * Returns the list of line indexes which contain prohibited content per this check (given an @@ -231,8 +231,10 @@ private data class MatchableFileContentCheck( }.map { (index, _) -> index } } - private fun isFileExempted(relativePath: String): Boolean = - relativePath in exemptedFileNames || exemptedFilePatterns.any { it.matches(relativePath) } + private fun isFileExempted(relativePath: String): Boolean { + return relativePath in exemptedFileNames + || exemptedFilePatterns.any { it.containsMatchIn(relativePath) } + } companion object { /** Returns a new [MatchableFileContentCheck] based on the specified [FileContentCheck]. */ diff --git a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt index 3f576389c91..25688daeba1 100644 --- a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt +++ b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt @@ -119,6 +119,7 @@ class RegexPatternValidationCheckTest { "Don't use Delegates; use a lateinit var or nullable primitive var default-initialized to" + " null, instead. Delegates uses reflection internally, have a non-trivial initialization" + " cost, and can cause breakages on KitKat devices. See #3939 for more context." + private val doNotUseProtoLibrary = "Don't use proto_library. Use oppia_proto_library instead." private val wikiReferenceNote = "Refer to https://github.com/oppia/oppia-android/wiki/Static-Analysis-Checks" + "#regexpatternvalidation-check for more details on how to fix this." @@ -1544,6 +1545,48 @@ class RegexPatternValidationCheckTest { ) } + @Test + fun testFileContent_buildFileUsesProtoLibrary_fileContentIsNotCorrect() { + val prohibitedContent = "proto_library(" + tempFolder.newFolder("testfiles", "domain", "src", "main") + val stringFilePath = "domain/src/main/BUILD" + tempFolder.newFile("testfiles/$stringFilePath").writeText(prohibitedContent) + + val exception = assertThrows(Exception::class) { + runScript() + } + + assertThat(exception).hasMessageThat().contains(REGEX_CHECK_FAILED_OUTPUT_INDICATOR) + assertThat(outContent.toString().trim()) + .isEqualTo( + """ + $stringFilePath:1: $doNotUseProtoLibrary + $wikiReferenceNote + """.trimIndent() + ) + } + + @Test + fun testFileContent_buildBazelFileUsesProtoLibrary_fileContentIsNotCorrect() { + val prohibitedContent = "proto_library(" + tempFolder.newFolder("testfiles", "domain", "src", "main") + val stringFilePath = "domain/src/main/BUILD.bazel" + tempFolder.newFile("testfiles/$stringFilePath").writeText(prohibitedContent) + + val exception = assertThrows(Exception::class) { + runScript() + } + + assertThat(exception).hasMessageThat().contains(REGEX_CHECK_FAILED_OUTPUT_INDICATOR) + assertThat(outContent.toString().trim()) + .isEqualTo( + """ + $stringFilePath:1: $doNotUseProtoLibrary + $wikiReferenceNote + """.trimIndent() + ) + } + @Test fun testFilenameAndContent_useProhibitedFileName_useProhibitedFileContent_multipleFailures() { tempFolder.newFolder("testfiles", "data", "src", "main") From e50a50f14e13a8f36885021aa3caccf14d4d53ca Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 13 Jan 2022 16:14:26 -0800 Subject: [PATCH 039/134] Lint fix. --- .../android/scripts/regex/RegexPatternValidationCheck.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/src/java/org/oppia/android/scripts/regex/RegexPatternValidationCheck.kt b/scripts/src/java/org/oppia/android/scripts/regex/RegexPatternValidationCheck.kt index a910ac65a4a..422c915adef 100644 --- a/scripts/src/java/org/oppia/android/scripts/regex/RegexPatternValidationCheck.kt +++ b/scripts/src/java/org/oppia/android/scripts/regex/RegexPatternValidationCheck.kt @@ -232,8 +232,8 @@ private data class MatchableFileContentCheck( } private fun isFileExempted(relativePath: String): Boolean { - return relativePath in exemptedFileNames - || exemptedFilePatterns.any { it.containsMatchIn(relativePath) } + return relativePath in exemptedFileNames || + exemptedFilePatterns.any { it.containsMatchIn(relativePath) } } companion object { From ec575b74ccca5d47097f4eb6810547878f68cb56 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 13 Jan 2022 17:03:22 -0800 Subject: [PATCH 040/134] Fix failing static checks. --- scripts/assets/test_file_exemptions.textproto | 4 ++-- utility/src/main/java/org/oppia/android/util/math/BUILD.bazel | 2 +- utility/src/test/java/org/oppia/android/util/math/BUILD.bazel | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index 8233c66c0d6..85d68bd005f 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -611,8 +611,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" @@ -710,6 +708,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/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index 4b84961d297..4ecd3e58e47 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -1,5 +1,5 @@ """ -TODO: document +General-purpose mathematics utilities, especially for supporting math-based interactions. """ load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") 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 index 493d89d66a0..313a5a1f751 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -1,5 +1,5 @@ """ -TODO: document +Tests for general-purpose mathematics utilities. """ load("//:oppia_android_test.bzl", "oppia_android_test") From 1268fc55bfabd268c9743f5efa36bd5752c41844 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 13 Jan 2022 19:27:47 -0800 Subject: [PATCH 041/134] Fix broken CI checks. Adds missing KDocs, test file exemptions, and fixes the Gradle build. --- model/src/main/proto/math.proto | 122 +++++- scripts/assets/test_file_exemptions.textproto | 4 + testing/build.gradle | 2 + .../oppia/android/testing/math/BUILD.bazel | 2 +- .../android/testing/math/FractionSubject.kt | 35 +- .../testing/math/MathEquationSubject.kt | 25 +- .../testing/math/MathExpressionSubject.kt | 369 ++++++++++++++++-- .../oppia/android/testing/math/RealSubject.kt | 26 +- 8 files changed, 532 insertions(+), 53 deletions(-) diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index 7dcbf780370..a8af5ba5bec 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -5,94 +5,186 @@ package model; option java_package = "org.oppia.android.app.model"; option java_multiple_files = true; -// Structure for a fraction object. +// Represents a fraction. +// +// Values of this proto can be analyzed using FractionSubject. message Fraction { + // Defines whether the fraction is negative. bool is_negative = 1; - int32 whole_number = 2; - int32 numerator = 3; - int32 denominator = 4; + + // Defines the whole number component of the fraction. + uint32 whole_number = 2; + + // Defines the numerator of the fraction. + uint32 numerator = 3; + + // Defines the denominator of the fraction. This should never be zero. + uint32 denominator = 4; } +// Represents a structured real value. +// +// Values of this proto can be analyzed using RealSubject. message Real { + // Defines type of real value. oneof real_type { + // Indicates that this real value is a fraction. Fraction rational = 1; - // Represents a decimal value. Technically these can sometimes be rational, but given IEEE-754 - // rounding errors we need to treat these values as irrational and non-factorable. + + // Indicates that this real value is a decimal. Technically these can sometimes be rational, but + // given IEEE-754 rounding errors and the difficulty of factoring fractions, many rational + // decimal values need to be treated as irrational and non-factorable. double irrational = 2; + + // Indicates that thi sreal value is an integer (as a special case of rational values since + // integers are easier to work with than fraction objects). Note that this isn't the only case + // where the real value can be an integer. It can also be an integer double value, or a fraction + // with only a whole number component. int32 integer = 3; } } -// Structure containing a ratio object for eg - [1,2,3] for 1:2:3. +// Represents a ratio, e.g. 1:2:3. message RatioExpression { - // List of components in a ratio. It's expected that list should have more than - // 1 element. + // List of components in a ratio. For example, the ratio 1:2:3 will have component values + // [1, 2, 3]. It's expected that list should have more than 1 element. repeated uint32 ratio_component = 1; } -// Represents a mathematical expression such as 1+2. The only expression currently supported is a -// binary operation. +// Represents a mathematical expression such as 1+2*7. Expressions are inherently recursive, so the +// overall expressiveness of this structure (& math expressions as a whole) is defined based on its +// constituent substructures. +// +// This structure is designed to represent both numeric and algebraic expressions. +// +// Values of this proto can be analyzed using MathExpressionSubject. message MathExpression { - // TODO: document inclusive - int32 parse_start_index = 1; - // TODO: document exclusive - int32 parse_end_index = 2; + // The index within the input text stream at which point the expression starts (it's an inclusive + // index). If both this and the end index are zero then no parsing information is included for + // this specific expression. + uint32 parse_start_index = 1; + + // The index within the input text stream at which point the expression ends, exclusively. If both + // this and the start index are zero then no parsing information is included for this specific + // expression. + uint32 parse_end_index = 2; + // The type of expression. oneof expression_type { + // Indicates that this expression is a real value. Real constant = 3; + + // Indicates that this expression is a variable (which is not valid for numeric-only + // expressions). string variable = 4; + + // Indicates that this expression is a binary operation between two sub-expressions. MathBinaryOperation binary_operation = 5; + + // Indicates that this expression is a unary operation that's operating on a sub-expression. MathUnaryOperation unary_operation = 6; + + // Indicates that this expression is a function call with a sub-expression argument. MathFunctionCall function_call = 7; + + // Indicates that this expression represents a nested group, e.g. 1+(2+3). MathExpression group = 8; } } +// Represents a binary operation like addition or multiplication. +// +// Values of this proto can be analyzed using MathExpressionSubject (within the context of a +// MathExpression). message MathBinaryOperation { + // Types of supported binary operations. enum Operator { + // Represents an unknown operator (which is never supported). OPERATOR_UNSPECIFIED = 0; + // Represents adding two values, e.g.: 1+x. ADD = 1; + // Represents subtracting two values, e.g.: x-2. SUBTRACT = 2; + // Represents multiplying two values, e.g.: x*y. MULTIPLY = 3; + // Represents dividing two values, e.g.: 1/x. DIVIDE = 4; + // Represents taking the exponentiation of one value by another, e.g.: x^2. EXPONENTIATE = 5; } + // The type of binary operation. Operator operator = 1; + + // The left-hand side of the operation, e.g. the '1' in 1+2. MathExpression left_operand = 2; + + // The right-hand side of the operation, e.g. the '2' in 1+2. MathExpression right_operand = 3; + + // Indicates whether this operation is implicit. This is currently only supported for + // multiplication, and helps represent expressions like '2x' (which should be treated as 2*x). bool is_implicit = 4; } +// Represents a unary operation like negation. +// +// Values of this proto can be analyzed using MathExpressionSubject (within the context of a +// MathExpression). message MathUnaryOperation { + // Types of supported unary operations. enum Operator { + // Represents an unknown operator (which is never supported). OPERATOR_UNSPECIFIED = 0; + // Represents negating a value, e.g.: -y. NEGATE = 1; + // Represents indicating a value as positive, e.g.: +y. POSITIVE = 2; } + // The type of unary operation, e.g. the '1' in -1. Operator operator = 1; + + // The operand being operated upon. MathExpression operand = 2; } +// Represents a function call, like square root. +// +// Values of this proto can be analyzed using MathExpressionSubject (within the context of a +// MathExpression). message MathFunctionCall { + // The types of supported function calls. enum FunctionType { + // Represents an unknown function (which is never supported). FUNCTION_UNSPECIFIED = 0; + + // Represents a square root operation, e.g. sqrt(4). SQUARE_ROOT = 1; } + // The type of function being called within this subexpression. FunctionType function_type = 1; + + // The subexpression being passed as an argument to the function. MathExpression argument = 2; } +// Represents a mathematical equation (generally algebraic) such as: 2x+3y=0. +// +// Values of this proto can be analyzed using MathEquationSubject. message MathEquation { + // The MathExpression representing the left-hand side of the equation, e.g. the '2x+3y' in + // 2x+3y=0. MathExpression left_side = 1; + + // The MathExpression representing the left-hand side of the equation, e.g. the '0' in 2x+3y=0. MathExpression right_side = 2; } diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index 85d68bd005f..6d52e0b504a 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -646,6 +646,10 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/Ge exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/ImageViewMatcher.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/KonfettiViewMatcher.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/DefineAppLanguageLocaleContext.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/mockito/MockitoKotlinHelper.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/network/ApiMockLoader.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/network/MockClassroomService.kt" diff --git a/testing/build.gradle b/testing/build.gradle index 6be1df02f89..02ab02ffdab 100644 --- a/testing/build.gradle +++ b/testing/build.gradle @@ -76,6 +76,7 @@ dependencies { 'com.google.dagger:dagger:2.24', 'com.google.protobuf:protobuf-javalite:3.17.3', 'com.google.truth:truth:1.1.3', + 'com.google.truth.extensions:truth-liteproto-extension:1.1.3', 'nl.dionsegijn:konfetti:1.2.5', 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.2', 'org.robolectric:robolectric:4.4', @@ -83,6 +84,7 @@ dependencies { 'org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version', 'org.mockito:mockito-core:2.19.0', project(":domain"), + project(":model"), project(":utility"), ) compileOnly( diff --git a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel index a1453dd4496..a178fd03604 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel @@ -1,5 +1,5 @@ """ -TODO: document +General testing utilities and truth subjects for math structures and utilities. """ load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") diff --git a/testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt index b256d7fd555..03c6ed4ef78 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt @@ -10,21 +10,52 @@ import com.google.common.truth.extensions.proto.LiteProtoSubject import org.oppia.android.app.model.Fraction import org.oppia.android.util.math.toDouble -class FractionSubject( +// TODO(#4097): Add tests for this class. + +/** + * Truth subject for verifying properties of [Fraction]s. + * + * Note that this class is also a [LiteProtoSubject] so other aspects of the underlying [Fraction] + * proto can be verified through inherited methods. + * + * Call [assertThat] to create the subject. + */ +class FractionSubject private constructor( metadata: FailureMetadata, private val actual: Fraction ) : LiteProtoSubject(metadata, actual) { + /** + * Returns a [BooleanSubject] to test [Fraction.getIsNegative]. This method never fails since the + * underlying property defaults to false if it's not defined in the fraction. + */ fun hasNegativePropertyThat(): BooleanSubject = assertThat(actual.isNegative) + /** + * Returns an [IntegerSubject] to test [Fraction.getWholeNumber]. This method never fails since + * the underlying property defaults to 0 if it's not defined in the fraction. + */ fun hasWholeNumberThat(): IntegerSubject = assertThat(actual.wholeNumber) + /** + * Returns an [IntegerSubject] to test [Fraction.getNumerator]. This method never fails since the + * underlying property defaults to 0 if it's not defined in the fraction. + */ fun hasNumeratorThat(): IntegerSubject = assertThat(actual.numerator) + /** + * Returns an [IntegerSubject] to test [Fraction.getDenominator]. This method never fails since + * the underlying property defaults to 0 if it's not defined in the fraction. + */ fun hasDenominatorThat(): IntegerSubject = assertThat(actual.denominator) - fun evaluatesToRealThat(): DoubleSubject = assertThat(actual.toDouble()) + /** + * Returns a [DoubleSubject] to test the converted double version of the fraction being + * represented by this subject. + */ + fun evaluatesToDoubleThat(): DoubleSubject = assertThat(actual.toDouble()) companion object { + /** Returns a new [FractionSubject] to verify aspects of the specified [Fraction] value. */ fun assertThat(actual: Fraction): FractionSubject = assertAbout(::FractionSubject).that(actual) } } diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt index ce24e1e08cc..b050a776c89 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt @@ -2,19 +2,42 @@ package org.oppia.android.testing.math import com.google.common.truth.FailureMetadata import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat import com.google.common.truth.extensions.proto.LiteProtoSubject import org.oppia.android.app.model.MathEquation +import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat -class MathEquationSubject( +// TODO(#4097): Add tests for this class. + +/** + * Truth subject for verifying properties of [MathEquation]s. + * + * Note that this class is also a [LiteProtoSubject] so other aspects of the underlying + * [MathEquation] proto can be verified through inherited methods. + * + * Call [assertThat] to create the subject. + */ +class MathEquationSubject private constructor( metadata: FailureMetadata, private val actual: MathEquation ) : LiteProtoSubject(metadata, actual) { + /** + * Returns a [MathExpressionSubject] to test [MathEquation.getLeftSide]. This method never fails + * since the underlying property defaults to a default proto if it's not defined in the equation. + */ fun hasLeftHandSideThat(): MathExpressionSubject = assertThat(actual.leftSide) + /** + * Returns a [MathExpressionSubject] to test [MathEquation.getRightSide]. This method never fails + * since the underlying property defaults to a default proto if it's not defined in the equation. + */ fun hasRightHandSideThat(): MathExpressionSubject = assertThat(actual.rightSide) companion object { + /** + * Returns a new [MathEquationSubject] to verify aspects of the specified [MathEquation] value. + */ fun assertThat(actual: MathEquation): MathEquationSubject = assertAbout(::MathEquationSubject).that(actual) } diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt index c9be134e209..d0394a0a3c1 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt @@ -16,102 +16,287 @@ import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE import org.oppia.android.app.model.MathFunctionCall import org.oppia.android.app.model.MathUnaryOperation import org.oppia.android.app.model.Real +import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat import org.oppia.android.testing.math.RealSubject.Companion.assertThat -// See: https://kotlinlang.org/docs/type-safe-builders.html. -class MathExpressionSubject( +// TODO(#4097): Add tests for this class. + +/** + * Truth subject for verifying properties of [MathExpression]s. + * + * This subject makes use of a custom Kotlin DSL to test the structure of an expression. This + * structure allows for recursive verification of the structure since the structure itself is + * recursive. Further, unchecked parts of the structure are not verified. See the following example + * to get an idea of the DSL for verifying expressions (see specific methods the comparator for all + * syntactical options): + * + * ```kotlin + * assertThat(expression).hasStructureThatMatches { + * addition { + * leftOperand { + * constant { + * withValueThat().isIntegerThat().isEqualTo(3) + * } + * } + * rightOperand { + * multiplication { + * leftOperand { + * constant { + * withValueThat().isIntegerThat().isEqualTo(4) + * } + * } + * rightOperand { + * negation { + * operand { + * constant { + * withValueThat().isIntegerThat().isEqualTo(5) + * } + * } + * } + * } + * } + * } + * } + * } + * ``` + * + * The above verifies the following structure: + * ``` + * + + * / \ + * 3 * + * / \ + * 4 - + * | + * 5 + * ``` + * + * (which would correspond to the expression 3+4*-5). + * + * Note that this class is also a [LiteProtoSubject] so other aspects of the underlying + * [MathExpression] proto can be verified through inherited methods. + * + * Call [assertThat] to create the subject. + */ +class MathExpressionSubject private constructor( metadata: FailureMetadata, private val actual: MathExpression ) : LiteProtoSubject(metadata, actual) { + /** + * Begins the structure syntax matcher. + * + * See [ExpressionComparator] for syntax. + */ fun hasStructureThatMatches(init: ExpressionComparator.() -> Unit) { - // TODO: maybe verify that all aspects are verified? ExpressionComparator.createFromExpression(actual).also(init) } - // TODO: update DSL to not have return values (since it's unnecessary). + /** + * DSL syntax provider for verifying the structure of a [MathExpression]. + * + * Note that per the proto definition of [MathExpression], this comparator can only represent one + * of the expression substructures (e.g. constant, variable, binary operations, and others). See + * the member methods for the different substructures that can be verified. + * + * Example syntax for verifying a constant: + * + * ```kotlin + * { + * constant { + * ... + * } + * } + * ``` + * + * is either verifying the root (i.e. via [hasStructureThatMatches]) or is for verifying + * a nested expression (such as through groups). + */ @ExpressionComparatorMarker class ExpressionComparator private constructor(private val expression: MathExpression) { - // TODO: convert to constant comparator? - fun constant(init: ConstantComparator.() -> Unit): ConstantComparator = + /** + * Begins structure matching for this expression as a constant per [MathExpression.getConstant]. + * + * This method will fail if the expression corresponding to the subject is not a constant. See + * [ConstantComparator] for example syntax. + */ + fun constant(init: ConstantComparator.() -> Unit) { ConstantComparator.createFromExpression(expression).also(init) + } - fun variable(init: VariableComparator.() -> Unit): VariableComparator = + /** + * Begins structure matching for this expression as a variable per [MathExpression.getVariable]. + * + * This method will fail if the expression corresponding to the subject is not a variable. See + * [VariableComparator] for example syntax. + */ + fun variable(init: VariableComparator.() -> Unit) { VariableComparator.createFromExpression(expression).also(init) + } - fun addition(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { - return BinaryOperationComparator.createFromExpression( + /** + * Begins structure matching for this expression as an addition operation per + * [MathExpression.getBinaryOperation]. + * + * This method will fail if the expression corresponding to the subject is not an addition + * operation. See [BinaryOperationComparator] for example syntax. + */ + fun addition(init: BinaryOperationComparator.() -> Unit) { + BinaryOperationComparator.createFromExpression( expression, expectedOperator = MathBinaryOperation.Operator.ADD ).also(init) } - fun subtraction(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { - return BinaryOperationComparator.createFromExpression( + /** + * Begins structure matching for this expression as a subtraction operation per + * [MathExpression.getBinaryOperation]. + * + * This method will fail if the expression corresponding to the subject is not a subtraction + * operation. See [BinaryOperationComparator] for example syntax. + */ + fun subtraction(init: BinaryOperationComparator.() -> Unit) { + BinaryOperationComparator.createFromExpression( expression, expectedOperator = MathBinaryOperation.Operator.SUBTRACT ).also(init) } - fun multiplication(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { - return BinaryOperationComparator.createFromExpression( + /** + * Begins structure matching for this expression as a multiplication operation per + * [MathExpression.getBinaryOperation]. + * + * This method will fail if the expression corresponding to the subject is not a multiplication + * operation. See [BinaryOperationComparator] for example syntax. + */ + fun multiplication(init: BinaryOperationComparator.() -> Unit) { + BinaryOperationComparator.createFromExpression( expression, expectedOperator = MathBinaryOperation.Operator.MULTIPLY ).also(init) } - fun division(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { - return BinaryOperationComparator.createFromExpression( + /** + * Begins structure matching for this expression as a division operation per + * [MathExpression.getBinaryOperation]. + * + * This method will fail if the expression corresponding to the subject is not a division + * operation. See [BinaryOperationComparator] for example syntax. + */ + fun division(init: BinaryOperationComparator.() -> Unit) { + BinaryOperationComparator.createFromExpression( expression, expectedOperator = MathBinaryOperation.Operator.DIVIDE ).also(init) } - fun exponentiation(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { - return BinaryOperationComparator.createFromExpression( + /** + * Begins structure matching for this expression as an exponentiation operation per + * [MathExpression.getBinaryOperation]. + * + * This method will fail if the expression corresponding to the subject is not an exponentiation + * operation. See [BinaryOperationComparator] for example syntax. + */ + fun exponentiation(init: BinaryOperationComparator.() -> Unit) { + BinaryOperationComparator.createFromExpression( expression, expectedOperator = MathBinaryOperation.Operator.EXPONENTIATE ).also(init) } - fun negation(init: UnaryOperationComparator.() -> Unit): UnaryOperationComparator { - return UnaryOperationComparator.createFromExpression( + /** + * Begins structure matching for this expression as a negation operation per + * [MathExpression.getUnaryOperation]. + * + * This method will fail if the expression corresponding to the subject is not a negation + * operation. See [UnaryOperationComparator] for example syntax. + */ + fun negation(init: UnaryOperationComparator.() -> Unit) { + UnaryOperationComparator.createFromExpression( expression, expectedOperator = MathUnaryOperation.Operator.NEGATE ).also(init) } - fun positive(init: UnaryOperationComparator.() -> Unit): UnaryOperationComparator { - return UnaryOperationComparator.createFromExpression( + /** + * Begins structure matching for this expression as a positive operation per + * [MathExpression.getUnaryOperation]. + * + * This method will fail if the expression corresponding to the subject is not a positive + * operation. See [UnaryOperationComparator] for example syntax. + */ + fun positive(init: UnaryOperationComparator.() -> Unit) { + UnaryOperationComparator.createFromExpression( expression, expectedOperator = MathUnaryOperation.Operator.POSITIVE ).also(init) } + /** + * Begins structure matching for this expression as a function call per + * [MathExpression.getFunctionCall]. + * + * This method will fail if the expression corresponding to the subject is not a function call. + * See [FunctionCallComparator] for example syntax. + */ fun functionCallTo( - type: MathFunctionCall.FunctionType, - init: FunctionCallComparator.() -> Unit - ): FunctionCallComparator { - return FunctionCallComparator.createFromExpression( - expression, - expectedFunctionType = type + type: MathFunctionCall.FunctionType, init: FunctionCallComparator.() -> Unit + ) { + FunctionCallComparator.createFromExpression( + expression, expectedFunctionType = type ).also(init) } - fun group(init: ExpressionComparator.() -> Unit): ExpressionComparator { - return createFromExpression(expression.group).also(init) + /** + * Begins structure matching for this expression as a group per [MathExpression.getGroup]. + * + * This method will fail if the expression corresponding to the subject is not a group. Example + * syntax: + * + * ```kotlin + * group { + * ... ... + * } + * ``` + * + * Groups refer to other expressions, so [ExpressionComparator] is used to verify constituent + * properties of the group. + */ + fun group(init: ExpressionComparator.() -> Unit) { + createFromExpression(expression.group).also(init) } internal companion object { + /** Returns a new [ExpressionComparator] corresponding to the specified [MathExpression]. */ fun createFromExpression(expression: MathExpression): ExpressionComparator = ExpressionComparator(expression) } } + /** + * DSL syntax provider for verifying constants. + * + * Example syntax: + * + * ```kotlin + * constant { + * withValueThat()... + * } + * ``` + * + * This comparator provides access to a [RealSubject] to verify the actual constant value. + */ @ExpressionComparatorMarker class ConstantComparator private constructor(private val constant: Real) { + /** + * Returns a [RealSubject] to verify the constant that's being represented by this comparator. + */ fun withValueThat(): RealSubject = assertThat(constant) internal companion object { + /** + * Returns a new [ConstantComparator] corresponding to the specified [MathExpression], + * verifying that it is, indeed, a constant. + */ fun createFromExpression(expression: MathExpression): ConstantComparator { assertThat(expression.expressionTypeCase).isEqualTo(CONSTANT) return ConstantComparator(expression.constant) @@ -119,11 +304,31 @@ class MathExpressionSubject( } } + /** + * DSL syntax provider for verifying variables. + * + * Example syntax: + * + * ```kotlin + * variable { + * withNameThat()... + * } + * ``` + * + * This comparator provides access to a [StringSubject] to verify the actual variable value. + */ @ExpressionComparatorMarker class VariableComparator private constructor(private val variableName: String) { + /** + * Returns a [StringSubject] to verify the variable that's being represented by this comparator. + */ fun withNameThat(): StringSubject = assertThat(variableName) internal companion object { + /** + * Returns a new [VariableComparator] corresponding to the specified [MathExpression], + * verifying that it is, indeed, a variable. + */ fun createFromExpression(expression: MathExpression): VariableComparator { assertThat(expression.expressionTypeCase).isEqualTo(VARIABLE) return VariableComparator(expression.variable) @@ -131,17 +336,56 @@ class MathExpressionSubject( } } + /** + * DSL syntax provider for verifying binary operations, like addition and multiplication. + * + * Example syntax: + * + * ```kotlin + * division { + * leftOperand { + * ... ... + * } + * + * rightOperand { + * ... ... + * } + * } + * ``` + * + * Both the left and right operands represent other [MathExpression]s. + */ @ExpressionComparatorMarker class BinaryOperationComparator private constructor( private val operation: MathBinaryOperation ) { - fun leftOperand(init: ExpressionComparator.() -> Unit): ExpressionComparator = + /** + * Begins structure matching this operation's left operand per + * [MathBinaryOperation.getLeftOperand] for the operation represented by this comparator. + * + * This method provides an [ExpressionComparator] to use to verify the constituent properties + * of the operand. + */ + fun leftOperand(init: ExpressionComparator.() -> Unit) { ExpressionComparator.createFromExpression(operation.leftOperand).also(init) + } - fun rightOperand(init: ExpressionComparator.() -> Unit): ExpressionComparator = + /** + * Begins structure matching this operation's right operand per + * [MathBinaryOperation.getRightOperand] for the operation represented by this comparator. + * + * This method provides an [ExpressionComparator] to use to verify the constituent properties + * of the operand. + */ + fun rightOperand(init: ExpressionComparator.() -> Unit) { ExpressionComparator.createFromExpression(operation.rightOperand).also(init) + } internal companion object { + /** + * Returns a new [BinaryOperationComparator] corresponding to the specified [MathExpression], + * verifying that it is, indeed, a binary operation with the specified operator. + */ fun createFromExpression( expression: MathExpression, expectedOperator: MathBinaryOperation.Operator @@ -155,14 +399,41 @@ class MathExpressionSubject( } } + /** + * DSL syntax provider for verifying unary operations, like negation. + * + * Example syntax: + * + * ```kotlin + * negation { + * operand { + * ... ... + * } + * } + * ``` + * + * The operation's operand represents another [MathExpression]. + */ @ExpressionComparatorMarker class UnaryOperationComparator private constructor( private val operation: MathUnaryOperation ) { - fun operand(init: ExpressionComparator.() -> Unit): ExpressionComparator = + /** + * Begins structure matching this operation's operand per [MathUnaryOperation.getOperand] for + * the operation represented by this comparator. + * + * This method provides an [ExpressionComparator] to use to verify the constituent properties + * of the operand. + */ + fun operand(init: ExpressionComparator.() -> Unit) { ExpressionComparator.createFromExpression(operation.operand).also(init) + } internal companion object { + /** + * Returns a new [UnaryOperationComparator] corresponding to the specified [MathExpression], + * verifying that it is, indeed, a unary operation with the specified operator. + */ fun createFromExpression( expression: MathExpression, expectedOperator: MathUnaryOperation.Operator @@ -176,14 +447,41 @@ class MathExpressionSubject( } } + /** + * DSL syntax provider for verifying function calls, like square root. + * + * Example syntax: + * + * ```kotlin + * functionCallTo(SQUARE_ROOT) { + * argument { + * ... ... + * } + * } + * ``` + * + * The function call's argument represents another [MathExpression]. + */ @ExpressionComparatorMarker class FunctionCallComparator private constructor( private val functionCall: MathFunctionCall ) { - fun argument(init: ExpressionComparator.() -> Unit): ExpressionComparator = + /** + * Begins structure matching the function call's argument per [MathFunctionCall.getArgument] for + * the operation represented by this comparator. + * + * This method provides an [ExpressionComparator] to use to verify the constituent properties + * of the function call's argument. + */ + fun argument(init: ExpressionComparator.() -> Unit) { ExpressionComparator.createFromExpression(functionCall.argument).also(init) + } internal companion object { + /** + * Returns a new [FunctionCallComparator] corresponding to the specified [MathExpression], + * verifying that it is, indeed, a function call with the function type. + */ fun createFromExpression( expression: MathExpression, expectedFunctionType: MathFunctionCall.FunctionType @@ -198,8 +496,13 @@ class MathExpressionSubject( } companion object { + // See: https://kotlinlang.org/docs/type-safe-builders.html for how the DSL definition works. @DslMarker private annotation class ExpressionComparatorMarker + /** + * Returns a new [MathExpressionSubject] to verify aspects of the specified [MathExpression] + * value. + */ fun assertThat(actual: MathExpression): MathExpressionSubject = assertAbout(::MathExpressionSubject).that(actual) } diff --git a/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt index 8f9edddda0b..cb541cb67cb 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt @@ -7,23 +7,46 @@ import com.google.common.truth.Truth.assertAbout import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import com.google.common.truth.extensions.proto.LiteProtoSubject +import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.Real import org.oppia.android.testing.math.FractionSubject.Companion.assertThat -class RealSubject( +// TODO(#4097): Add tests for this class. + +/** + * Truth subject for verifying properties of [Real]s. + * + * Note that this class is also a [LiteProtoSubject] so other aspects of the underlying [Real] proto + * can be verified through inherited methods. + * + * Call [assertThat] to create the subject. + */ +class RealSubject private constructor( metadata: FailureMetadata, private val actual: Real ) : LiteProtoSubject(metadata, actual) { + /** + * Returns a [FractionSubject] to test [Real.getRational]. This will fail if the [Real] pertaining + * to this subject is not of type rational. + */ fun isRationalThat(): FractionSubject { verifyTypeToBe(Real.RealTypeCase.RATIONAL) return assertThat(actual.rational) } + /** + * Returns a [DoubleSubject] to test [Real.getIrrational]. This will fail if the [Real] pertaining + * to this subject is not of type irrational. + */ fun isIrrationalThat(): DoubleSubject { verifyTypeToBe(Real.RealTypeCase.IRRATIONAL) return assertThat(actual.irrational) } + /** + * Returns a [IntegerSubject] to test [Real.getInteger]. This will fail if the [Real] pertaining + * to this subject is not of type integer. + */ fun isIntegerThat(): IntegerSubject { verifyTypeToBe(Real.RealTypeCase.INTEGER) return assertThat(actual.integer) @@ -36,6 +59,7 @@ class RealSubject( } companion object { + /** Returns a new [RealSubject] to verify aspects of the specified [Real] value. */ fun assertThat(actual: Real): RealSubject = assertAbout(::RealSubject).that(actual) } } From 977eb9e077fcc43b45a4b5b17a6db92c5d24f3b9 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 13 Jan 2022 19:28:55 -0800 Subject: [PATCH 042/134] Lint fixes. --- .../org/oppia/android/testing/math/MathExpressionSubject.kt | 5 +++-- .../main/java/org/oppia/android/testing/math/RealSubject.kt | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt index d0394a0a3c1..c5eb730c868 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt @@ -23,7 +23,7 @@ import org.oppia.android.testing.math.RealSubject.Companion.assertThat /** * Truth subject for verifying properties of [MathExpression]s. - * + * * This subject makes use of a custom Kotlin DSL to test the structure of an expression. This * structure allows for recursive verification of the structure since the structure itself is * recursive. Further, unchecked parts of the structure are not verified. See the following example @@ -239,7 +239,8 @@ class MathExpressionSubject private constructor( * See [FunctionCallComparator] for example syntax. */ fun functionCallTo( - type: MathFunctionCall.FunctionType, init: FunctionCallComparator.() -> Unit + type: MathFunctionCall.FunctionType, + init: FunctionCallComparator.() -> Unit ) { FunctionCallComparator.createFromExpression( expression, expectedFunctionType = type diff --git a/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt index cb541cb67cb..bb8a180b306 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt @@ -7,7 +7,6 @@ import com.google.common.truth.Truth.assertAbout import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import com.google.common.truth.extensions.proto.LiteProtoSubject -import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.Real import org.oppia.android.testing.math.FractionSubject.Companion.assertThat From eed21d53c0146aefec49d771b7935404d2554dfb Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 13 Jan 2022 21:27:59 -0800 Subject: [PATCH 043/134] Add docs & exempted tests. --- model/src/main/proto/math.proto | 71 +++- scripts/assets/test_file_exemptions.textproto | 1 + .../math/ComparableOperationListSubject.kt | 323 +++++++++++++++++- 3 files changed, 370 insertions(+), 25 deletions(-) diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index aa341f58161..dfe00605efe 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -189,55 +189,104 @@ message MathEquation { MathExpression right_side = 2; } -// Represents a list of comparable mathematics operations. 'Comparable' here means that this -// structure provides a trivial way to compare commutative operations (i.e. by extracting terms from -// multiple subsequent commutative operations into lists that can be deterministically sorted). This -// structure is meant to provide a means to compare two expressions without considering -// associativity or commutativity (though the latter requires the operation lists stored within this -// structure to be sorted before using standard proto equals checking). +// Represents a list of comparable mathematics operations. +// +// 'Comparable' here means that this structure provides a trivial way to compare commutative and +// associative operations (i.e. by extracting terms from multiple subsequent commutative & +// associative operations into lists that can be deterministically sorted). This structure is meant +// to provide a means to compare two expressions without considering associativity or commutativity +// (though the latter requires the operation lists stored within this structure to be sorted before +// using standard proto equals checking). Also note that care must be taken when performing equality +// checking since this structure can contain floating point values that require an epsilon check to +// approximate equality. message ComparableOperationList { + // An operation that can be compared in a way that does not change the value based on + // commutativity or associativity. message ComparableOperation { - // Treat this operation (e.g. x) as negated (e.g. -x). + // Indicates that this operation (e.g. x) should be treated as negated (e.g. -x). bool is_negated = 1; - // Treat this operation (e.g. x) as a multiplicative inverse (e.g. 1/x). + // Indicates that this operation (e.g. x) should be treated as a multiplicative inverse + // (e.g. 1/x). bool is_inverted = 2; + // The supported comparison types. oneof comparison_type { + // Indicates that this operation is a commutative accumulation (that is, a list of subsequent + // operations of the same type that are commutative, e.g. addition or multiplication). CommutativeAccumulation commutative_accumulation = 3; + + // Indicates that this operation is a non-commutative operation and thus cannot be + // accumulated (e.g. exponentiation). NonCommutativeOperation non_commutative_operation = 4; + + // Indicates that this operation represents a constant value. + Real constant_term = 5; + + // Indicates that this operation represents a variable. string variable_term = 6; } } - // Represents an accumulation of operations (such as a summation or product). This helps simplify - // comparison across commutative boundaries by collecting terms into sortable lists, such as the - // expression 1+2+3 becoming [1,2,3] and trivially comparable to [3,2,1] from 3+2+1. + + // Represents an accumulation of operations (such as a summation or product). + // + // This helps simplify comparison across commutative and associative boundaries by collecting + // terms into sortable lists, such as the expression 1+2+3 becoming [1,2,3] and trivially + // comparable to [3,2,1] from 3+2+1 (once sorted). // // Subsequent subtractions are treated as additions with each term arithmetically negated (i.e. // f(x)=-x). Similarly, divisions are considered multiplications with each divisor being // multiplicatively inverted (i.e. the reciprocal function: f(x)=1/x). message CommutativeAccumulation { + // The types of supported accumulations. enum AccumulationType { + // Represents an unsupported accumulation type (which is always invalid). ACCUMULATION_TYPE_UNSPECIFIED = 0; + + // Represents an accumulation of sums, e.g. 1+2+3. SUMMATION = 1; + + // Represents an accumulation of products, e.g. 2*3*4. PRODUCT = 2; } + // The type of this accumulation. AccumulationType accumulation_type = 1; + + // The list of operations being combined in this accumulation (which can be subsequent + // accumulations in more complex expressions). repeated ComparableOperation combined_operations = 2; } + + // Represents a non-commutative operation (such as exponentiation or square roots). + // + // Operations represented by this structure cannot be combined in an accumulation which means they + // can't be "internally" sorted (in order to maintain the commutativity and associativity of the + // operation). message NonCommutativeOperation { + // The types of supported non-commutative operations. oneof operation_type { + // Indicates that this is an exponentiation operation. BinaryOperation exponentiation = 1; + + // Indicates that this is a square root operation with this operation representing the + // argument passed to the square root function. ComparableOperation square_root = 2; } + // Represents a non-commutative binary operation, such as exponentiation. message BinaryOperation { + // The left-hand side of the operation (which itself is another operation), e.g. the '2' in + // 2^3. ComparableOperation left_operand = 1; + + // The right-hand side of the operation (which itself is another operation), e.g. the '3' in + // 2^3. ComparableOperation right_operand = 2; } } + // The root of the operation list (i.e. the lowest precedent operation of the expression). ComparableOperation root_operation = 1; } diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index 6d52e0b504a..fa79d219d56 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -646,6 +646,7 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/Ge exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/ImageViewMatcher.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/KonfettiViewMatcher.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/DefineAppLanguageLocaleContext.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt" diff --git a/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt index ce32ec28d2b..3d71f819677 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt @@ -13,61 +13,226 @@ import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.C import org.oppia.android.app.model.Real import org.oppia.android.testing.math.RealSubject.Companion.assertThat -class ComparableOperationListSubject( +// TODO(#4098): Add tests for this class. + +/** + * Truth subject for verifying properties of [ComparableOperationList]s. + * + * This subject makes use of a custom Kotlin DSL to test the structure of a comparable operation + * list. This structure allows for recursive verification of the structure since the structure + * itself is recursive. Further, unchecked parts of the structure are not verified. See the + * following example to get an idea of the DSL for verifying operations (see specific methods of the + * comparators for all syntactical options): + * + * ```kotlin + * assertThat(comparableOperationList).hasStructureThatMatches { + * hasNegatedPropertyThat().isFalse() + * hasInvertedPropertyThat().isFalse() + * commutativeAccumulationWithType(SUMMATION) { + * hasOperandCountThat().isEqualTo(3) + * index(0) { + * hasNegatedPropertyThat().isFalse() + * hasInvertedPropertyThat().isFalse() + * constantTerm { + * withValueThat().isIntegerThat().isEqualTo(1) + * } + * } + * index(1) { + * hasNegatedPropertyThat().isFalse() + * hasInvertedPropertyThat().isFalse() + * constantTerm { + * withValueThat().isIntegerThat().isEqualTo(3) + * } + * } + * index(2) { + * hasNegatedPropertyThat().isFalse() + * hasInvertedPropertyThat().isFalse() + * constantTerm { + * withValueThat().isIntegerThat().isEqualTo(4) + * } + * } + * } + * } + * ``` + * + * The above verifies the following structure corresponding to the expression 1+3+4. + * + * Note that this class is also a [LiteProtoSubject] so other aspects of the underlying + * [ComparableOperationList] proto can be verified through inherited methods. + * + * Call [assertThat] to create the subject. + */ +class ComparableOperationListSubject private constructor( metadata: FailureMetadata, private val actual: ComparableOperationList ) : LiteProtoSubject(metadata, actual) { + /** + * Begins the structure syntax matcher for the root of the [ComparableOperationList] corresponding + * to this subject (per [ComparableOperationList.getRootOperation]). + * + * See [ComparableOperationComparator] for syntax. + */ fun hasStructureThatMatches(init: ComparableOperationComparator.() -> Unit) { ComparableOperationComparator.createFrom(actual.rootOperation).also(init) } + /** + * DSL syntax provider for verifying the structure of a [ComparableOperation]. + * + * Note that per the proto definition of [ComparableOperation], this comparator can only represent + * one of the operation substructures (e.g. constant, variable, commutative accumulations, and + * others). See the member methods for the different substructures that can be verified. + * + * Example syntax for verifying a constant term: + * + * ```kotlin + * { + * constantTerm { + * ... + * } + * } + * ``` + * + * is either verifying the root (i.e. via [hasStructureThatMatches]) or is for verifying + * a nested operation (such as through a non-commutative operation). + */ @ComparableOperationComparatorMarker class ComparableOperationComparator private constructor( private val operation: ComparableOperation ) { + /** + * Returns a [BooleanSubject] to test [ComparableOperation.getIsNegated]. + * + * This method never fails since the underlying property defaults to false if it's not defined + * in the fraction. + */ fun hasNegatedPropertyThat(): BooleanSubject = assertThat(operation.isNegated) + /** + * Returns a [BooleanSubject] to test [ComparableOperation.getIsNegated]. + * + * This method never fails since the underlying property defaults to false if it's not defined + * in the fraction. + */ fun hasInvertedPropertyThat(): BooleanSubject = assertThat(operation.isInverted) + /** + * Begins structure matching for this operation as a commutative accumulation per + * [ComparableOperation.getCommutativeAccumulation]. + * + * This method will fail if the represented operation is not a commutative accumulation with the + * specified type. See [CommutativeAccumulationComparator] for example syntax. + */ fun commutativeAccumulationWithType( type: ComparableOperationList.CommutativeAccumulation.AccumulationType, init: CommutativeAccumulationComparator.() -> Unit - ): CommutativeAccumulationComparator = + ) { CommutativeAccumulationComparator.createFrom(type, operation).also(init) + } + /** + * Begins structure matching for this operation as a non-commutative operation per + * [ComparableOperation.getNonCommutativeOperation]. + * + * This method will fail if the represented operation is not a non-commutative operation. See + * [NonCommutativeOperationComparator] for example syntax. + */ fun nonCommutativeOperation( init: NonCommutativeOperationComparator.() -> Unit - ): NonCommutativeOperationComparator = + ) { NonCommutativeOperationComparator.createFrom(operation).also(init) + } - fun constantTerm(init: ConstantTermComparator.() -> Unit): ConstantTermComparator = + /** + * Begins structure matching for this operation as a constant term per + * [ComparableOperation.getConstantTerm]. + * + * This method will fail if the represented operation is not a constant term. See + * [ConstantTermComparator] for example syntax. + */ + fun constantTerm(init: ConstantTermComparator.() -> Unit) { ConstantTermComparator.createFrom(operation).also(init) + } - fun variableTerm(init: VariableTermComparator.() -> Unit): VariableTermComparator = + /** + * Begins structure matching for this operation as a variable term per + * [ComparableOperation.getVariableTerm]. + * + * This method will fail if the represented operation is not a variable term. See + * [VariableTermComparator] for example syntax. + */ + fun variableTerm(init: VariableTermComparator.() -> Unit) { VariableTermComparator.createFrom(operation).also(init) + } internal companion object { + /** + * Returns a new [ComparableOperationComparator] corresponding to the specified + * [ComparableOperation]. + */ fun createFrom(operation: ComparableOperation): ComparableOperationComparator = ComparableOperationComparator(operation) } } + /** + * DSL syntax provider for verifying commutative accumulations such as summations or products. + * + * Example syntax: + * + * ```kotlin + * commutativeAccumulationWithType(PRODUCT) { + * hasOperandCountThat().isEqualTo(2) + * index(0) { + * ... ... + * } + * index(1) { + * ... ... + * } + * } + * ``` + * + * As demonstrated, an accumulation represents a list of comparable operations which may be other + * accumulations (though it's guaranteed per the structure that nested accumulations will never be + * the same type), non-commutative operations, constants, or variables. List entries are also + * verified in order. + */ @ComparableOperationComparatorMarker class CommutativeAccumulationComparator private constructor( private val accumulation: ComparableOperationList.CommutativeAccumulation ) { + /** + * Returns a [IntegerSubject] to test + * [ComparableOperationList.CommutativeAccumulation.getCombinedOperationsCount]. + * + * This method never fails since the underlying property defaults to 0 if there are no + * operations in the accumulation. + */ fun hasOperandCountThat(): IntegerSubject = assertThat(accumulation.combinedOperationsCount) + /** + * Begins structure matching for the operation at the specified index within the outer operation + * represented by this comparator. + * + * This method will fail if the operation corresponding to the subject does not have a + * sub-operation at the specified index. See [ComparableOperationComparator] for available + * verification functionality for each indexed operation. + */ fun index( index: Int, init: ComparableOperationComparator.() -> Unit - ): ComparableOperationComparator { - return ComparableOperationComparator.createFrom( + ) { + ComparableOperationComparator.createFrom( accumulation.combinedOperationsList[index] ).also(init) } internal companion object { + /** + * Returns a new [CommutativeAccumulationComparator] corresponding to the specified + * [ComparableOperation], verifying that it is, indeed, a commutative accumulation of the + * specified type. + */ fun createFrom( type: ComparableOperationList.CommutativeAccumulation.AccumulationType, operation: ComparableOperation @@ -80,22 +245,62 @@ class ComparableOperationListSubject( } } + /** + * DSL syntax provider for verifying non-commutative operations such as exponentiation or square + * roots. + * + * Example syntax: + * + * ```kotlin + * nonCommutativeOperation { + * ... ... + * } + * ``` + */ @ComparableOperationComparatorMarker class NonCommutativeOperationComparator private constructor( private val operation: ComparableOperationList.NonCommutativeOperation ) { - fun exponentiation(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + /** + * Begins structure matching for this operation as an exponentiation per + * [ComparableOperationList.NonCommutativeOperation.getExponentiation]. + * + * This method will fail if the operation corresponding to the subject is not an exponentiation. + * See [BinaryOperationComparator] for specifics on the operation comparator used here. Example + * syntax: + * + * ```kotlin + * exponentiation { + * ... ... + * } + * ``` + */ + fun exponentiation(init: BinaryOperationComparator.() -> Unit) { verifyTypeAs( ComparableOperationList.NonCommutativeOperation.OperationTypeCase.EXPONENTIATION ) - return BinaryOperationComparator.createFrom(operation.exponentiation).also(init) + BinaryOperationComparator.createFrom(operation.exponentiation).also(init) } + /** + * Begins structure matching for this operation as a square root operation per + * [ComparableOperationList.NonCommutativeOperation.getSquareRoot]. + * + * This method will fail if the operation corresponding to the subject is not a square root. The + * argument is another [ComparableOperation] hence the utilization of + * [ComparableOperationComparator]. Example syntax: + * + * ```kotlin + * squareRootWithArgument { + * ... ... + * } + * ``` + */ fun squareRootWithArgument( init: ComparableOperationComparator.() -> Unit - ): ComparableOperationComparator { + ) { verifyTypeAs(ComparableOperationList.NonCommutativeOperation.OperationTypeCase.SQUARE_ROOT) - return ComparableOperationComparator.createFrom(operation.squareRoot).also(init) + ComparableOperationComparator.createFrom(operation.squareRoot).also(init) } private fun verifyTypeAs( @@ -105,6 +310,11 @@ class ComparableOperationListSubject( } internal companion object { + /** + * Returns a new [NonCommutativeOperationComparator] corresponding to the specified + * [ComparableOperation], verifying that it is, indeed, a non-commutative operation of the + * specified type. + */ fun createFrom(operation: ComparableOperation): NonCommutativeOperationComparator { assertThat(operation.comparisonTypeCase) .isEqualTo(ComparisonTypeCase.NON_COMMUTATIVE_OPERATION) @@ -113,34 +323,95 @@ class ComparableOperationListSubject( } } + /** + * DSL syntax provider for verifying non-commutative binary operations (e.g. exponentiation). + * + * Example syntax: + * + * ```kotlin + * { + * leftOperand { + * ... ... + * } + * rightOperand { + * ... ... + * } + * } + * ``` + * + * Both the left and right operands represent other [ComparableOperation]s. Further, this + * comparator is used in conjunction with [NonCommutativeOperationComparator]. + */ @ComparableOperationComparatorMarker class BinaryOperationComparator private constructor( private val operation: ComparableOperationList.NonCommutativeOperation.BinaryOperation ) { + /** + * Begins structure matching this operation's left operand per + * [ComparableOperationList.NonCommutativeOperation.BinaryOperation.getLeftOperand] for the + * operation represented by this comparator. + * + * This method provides an [ComparableOperationComparator] to use to verify the constituent + * properties of the operand. + */ fun leftOperand( init: ComparableOperationComparator.() -> Unit - ): ComparableOperationComparator = + ) { ComparableOperationComparator.createFrom(operation.leftOperand).also(init) + } + /** + * Begins structure matching this operation's right operand per + * [ComparableOperationList.NonCommutativeOperation.BinaryOperation.getRightOperand] for the + * operation represented by this comparator. + * + * This method provides an [ComparableOperationComparator] to use to verify the constituent + * properties of the operand. + */ fun rightOperand( init: ComparableOperationComparator.() -> Unit - ): ComparableOperationComparator = + ) { ComparableOperationComparator.createFrom(operation.rightOperand).also(init) + } internal companion object { + /** + * Returns a new [BinaryOperationComparator] corresponding to the specified non-commutative + * binary operation. + */ fun createFrom( operation: ComparableOperationList.NonCommutativeOperation.BinaryOperation ): BinaryOperationComparator = BinaryOperationComparator(operation) } } + /** + * DSL syntax provider for verifying constants. + * + * Example syntax: + * + * ```kotlin + * constantTerm { + * withValueThat()... + * } + * ``` + * + * This comparator provides access to a [RealSubject] to verify the actual constant value. + */ @ComparableOperationComparatorMarker class ConstantTermComparator private constructor( private val constant: Real ) { + /** + * Returns a [RealSubject] to verify the constant that's being represented by this comparator. + */ fun withValueThat(): RealSubject = assertThat(constant) internal companion object { + /** + * Returns a new [ConstantTermComparator] corresponding to the specified + * [ComparableOperation], verifying that it is, indeed, a constant term. + */ fun createFrom(operation: ComparableOperation): ConstantTermComparator { assertThat(operation.comparisonTypeCase).isEqualTo(ComparisonTypeCase.CONSTANT_TERM) return ConstantTermComparator(operation.constantTerm) @@ -148,13 +419,33 @@ class ComparableOperationListSubject( } } + /** + * DSL syntax provider for verifying variables. + * + * Example syntax: + * + * ```kotlin + * variableTerm { + * withNameThat()... + * } + * ``` + * + * This comparator provides access to a [StringSubject] to verify the actual variable value. + */ @ComparableOperationComparatorMarker class VariableTermComparator private constructor( private val variableName: String ) { + /** + * Returns a [StringSubject] to verify the variable that's being represented by this comparator. + */ fun withNameThat(): StringSubject = assertThat(variableName) internal companion object { + /** + * Returns a new [VariableTermComparator] corresponding to the specified + * [ComparableOperation], verifying that it is, indeed, a variable term. + */ fun createFrom(operation: ComparableOperation): VariableTermComparator { assertThat(operation.comparisonTypeCase).isEqualTo(ComparisonTypeCase.VARIABLE_TERM) return VariableTermComparator(operation.variableTerm) @@ -163,9 +454,13 @@ class ComparableOperationListSubject( } companion object { - // See: https://kotlinlang.org/docs/type-safe-builders.html. + // See: https://kotlinlang.org/docs/type-safe-builders.html for how the DSL definition works. @DslMarker private annotation class ComparableOperationComparatorMarker + /** + * Returns a new [ComparableOperationListSubject] to verify aspects of the specified + * [ComparableOperationList] value. + */ fun assertThat(actual: ComparableOperationList): ComparableOperationListSubject = assertAbout(::ComparableOperationListSubject).that(actual) } From 080e7dd048265b892d5a53451f3d02c3c892a9e0 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 13 Jan 2022 21:39:27 -0800 Subject: [PATCH 044/134] Remove blank line. --- model/src/main/proto/math.proto | 1 - 1 file changed, 1 deletion(-) diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index dfe00605efe..9a97c7821ad 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -221,7 +221,6 @@ message ComparableOperationList { NonCommutativeOperation non_commutative_operation = 4; // Indicates that this operation represents a constant value. - Real constant_term = 5; // Indicates that this operation represents a variable. From 904aad89216f975274c42367c4a57f848f32d9ab Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 18 Jan 2022 18:29:58 -0800 Subject: [PATCH 045/134] Add docs + tests. --- model/src/main/proto/math.proto | 12 + scripts/assets/test_file_exemptions.textproto | 1 + .../android/testing/math/PolynomialSubject.kt | 83 ++- .../android/util/math/FloatExtensions.kt | 15 +- .../android/util/math/FractionExtensions.kt | 9 +- .../android/util/math/PolynomialExtensions.kt | 6 + .../android/util/math/RatioExtensions.kt | 5 + .../oppia/android/util/math/RealExtensions.kt | 37 +- .../org/oppia/android/util/math/BUILD.bazel | 73 +++ .../android/util/math/FloatExtensionsTest.kt | 155 +++++ .../util/math/FractionExtensionsTest.kt | 530 ++++++++++++++++++ .../util/math/PolynomialExtensionsTest.kt | 390 +++++++++++++ .../android/util/math/RatioExtensionsTest.kt | 4 +- .../android/util/math/RealExtensionsTest.kt | 414 ++++++++++++++ 14 files changed, 1720 insertions(+), 14 deletions(-) create mode 100644 utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/FractionExtensionsTest.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index 877a070b539..4adf75cb83e 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -291,15 +291,27 @@ message ComparableOperationList { ComparableOperation root_operation = 1; } +// Represents a polynomial, e.g.: 2x^3+3x-y+7. message Polynomial { + // The list of terms in this polynomial, e.g. the '2x^3', '3x', '-y', and '-7' in 2x^3+3x-y+7. repeated Term term = 1; + // Represents a polynomial term, i.e. a real coefficient multiplied by one or more variables, each + // of which may have a power >= 1. message Term { + // The coefficient of this term (which may be zero or negative), e.g. '2' in '2x^3'. Real coefficient = 1; + + // The variables of this term. This list can be zero or more variables long (where zero + // variables indicate a constant polynomial term). repeated Variable variable = 2; + // A variable within the term, that is, a variable with a positive power. message Variable { + // The name of the variable, e.g. 'x' in 'x^3'. string name = 1; + + // The power of the variable, e.g. '3' in 'x^3'. uint32 power = 2; } } diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index fa79d219d56..ae6a4f367b2 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -649,6 +649,7 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/Defin exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/mockito/MockitoKotlinHelper.kt" diff --git a/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt index bb4a3d970d5..23dc2b479a7 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt @@ -8,11 +8,22 @@ import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import com.google.common.truth.extensions.proto.LiteProtoSubject import org.oppia.android.app.model.Polynomial +import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.oppia.android.util.math.getConstant import org.oppia.android.util.math.isConstant import org.oppia.android.util.math.toPlainText +// TODO(#4100): Add tests for this class. + +/** + * Truth subject for verifying properties of [Polynomial]s. + * + * Note that this class is also a [LiteProtoSubject] so other aspects of the underlying [Polynomial] + * proto can be verified through inherited methods. + * + * Call [assertThat] to create the subject. + */ class PolynomialSubject( metadata: FailureMetadata, private val actual: Polynomial? @@ -24,28 +35,48 @@ class PolynomialSubject( } } + /** Verifies that the represented [Polynomial] is null (i.e. not a valid polynomial). */ fun isNotValidPolynomial() { - // TODO: use toPlainText here. assertWithMessage( "Expected polynomial to be undefined, but was: ${actual?.toPlainText()}" ).that(actual).isNull() } + /** + * Verifies that the represented [Polynomial] is a constant (i.e. [Polynomial.isConstant] and + * returns a [RealSubject] to verify the value of the constant polynomial. + */ fun isConstantThat(): RealSubject { - // TODO: use toPlainText here. - assertWithMessage("Expected polynomial to be constant, but was: $nonNullActual") + assertWithMessage("Expected polynomial to be constant, but was: ${nonNullActual.toPlainText()}") .that(nonNullActual.isConstant()) .isTrue() return assertThat(nonNullActual.getConstant()) } + /** + * Returns an [IntegerSubject] to test [Polynomial.getTermCount]. + * + * This method never fails since the underlying property defaults to 0 if there are no terms + * defined in the polynomial (unless the polynomial is null). + */ fun hasTermCountThat(): IntegerSubject = assertThat(nonNullActual.termCount) + /** + * Returns a [PolynomialTermSubject] to test [Polynomial.getTerm] for the specified index. + * + * This method throws if the index doesn't correspond to a valid term. Callers should first verify + * the term count using [hasTermCountThat]. + */ fun term(index: Int): PolynomialTermSubject = assertThat(nonNullActual.termList[index]) + /** + * Returns a [StringSubject] to test the plain-text representation of the [Polynomial] (i.e. via + * [Polynomial.toPlainText]). + */ fun evaluatesToPlainTextThat(): StringSubject = assertThat(nonNullActual.toPlainText()) companion object { + /** Returns a new [PolynomialSubject] to verify aspects of the specified [Polynomial] value. */ fun assertThat(actual: Polynomial?): PolynomialSubject = assertAbout(::PolynomialSubject).that(actual) @@ -56,24 +87,70 @@ class PolynomialSubject( assertAbout(::PolynomialTermVariableSubject).that(actual) } + /** + * Truth subject for verifying properties of [Polynomial.Term]s. + * + * Note that this class is also a [LiteProtoSubject] so other aspects of the underlying + * [Polynomial.Term] proto can be verified through inherited methods. + */ class PolynomialTermSubject( metadata: FailureMetadata, private val actual: Polynomial.Term ) : LiteProtoSubject(metadata, actual) { + /** + * Returns a [RealSubject] to test [Polynomial.Term.getCoefficient] for the represented term. + * + * This method never fails since the underlying property defaults to a default instance if it's + * not defined in the term. + */ fun hasCoefficientThat(): RealSubject = assertThat(actual.coefficient) + /** + * Returns an [IntegerSubject] to test [Polynomial.Term.getVariableCount] for the represented + * term. + * + * This method never fails since the underlying property defaults to 0 if there are no variables + * in the represented term. + */ fun hasVariableCountThat(): IntegerSubject = assertThat(actual.variableCount) + /** + * Returns a [PolynomialTermVariableSubject] to test [Polynomial.Term.getVariable] for the + * specified index. + * + * This method throws if the index doesn't correspond to a valid variable. Callers should first + * verify the variable count using [hasVariableCountThat]. + */ fun variable(index: Int): PolynomialTermVariableSubject = assertThat(actual.variableList[index]) } + /** + * Truth subject for verifying properties of [Polynomial.Term.Variable]s. + * + * Note that this class is also a [LiteProtoSubject] so other aspects of the underlying + * [Polynomial.Term.Variable] proto can be verified through inherited methods. + */ class PolynomialTermVariableSubject( metadata: FailureMetadata, private val actual: Polynomial.Term.Variable ) : LiteProtoSubject(metadata, actual) { + /** + * Returns a [StringSubject] to test [Polynomial.Term.Variable.getName] for the represented + * variable. + * + * This method never fails since the underlying property defaults to empty string if it's not + * defined in the variable. + */ fun hasNameThat(): StringSubject = assertThat(actual.name) + /** + * Returns an [IntegerSubject] to test [Polynomial.Term.Variable.getPower] for the represented + * variable. + * + * This method never fails since the underlying property defaults to 0 if it's not defined in + * the variable. + */ fun hasPowerThat(): IntegerSubject = assertThat(actual.power) } } diff --git a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt index 2ca5ece9da3..27ac002c08c 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt @@ -2,17 +2,26 @@ package org.oppia.android.util.math import kotlin.math.abs -/** The error margin used for float equality by [Float.approximatelyEquals]. */ +/** The error margin used for approximately [Float] and [Double] equality checking. */ const val FLOAT_EQUALITY_INTERVAL = 1e-5 -/** Returns whether this float approximately equals another based on a consistent epsilon value. */ +/** + * Returns whether this float approximately equals another based on a consistent epsilon value + * ([FLOAT_EQUALITY_INTERVAL]). + */ fun Float.approximatelyEquals(other: Float): Boolean { return abs(this - other) < FLOAT_EQUALITY_INTERVAL } -/** Returns whether this double approximately equals another based on a consistent epsilon value. */ +/** Returns whether this double approximately equals another based on a consistent epsilon value + * ([FLOAT_EQUALITY_INTERVAL]). + */ fun Double.approximatelyEquals(other: Double): Boolean { return abs(this - other) < FLOAT_EQUALITY_INTERVAL } +/** + * Returns a string representation of this [Double] that keeps the double in pure decimal and never + * relies on scientific notation (unlike [Double.toString]). + */ fun Double.toPlainString(): String = toBigDecimal().toPlainString() diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index 1da9fef1857..d4881613346 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -32,9 +32,10 @@ fun Fraction.toDouble(): Double { */ fun Fraction.toAnswerString(): String { return when { - isOnlyWholeNumber() -> { - // Fraction is only a whole number. - if (isNegative) "-$wholeNumber" else "$wholeNumber" + // Fraction is only a whole number. + isOnlyWholeNumber() -> when (wholeNumber) { + 0 -> "0" // 0 is always 0 regardless of its negative sign. + else -> if (isNegative) "-$wholeNumber" else "$wholeNumber" } wholeNumber == 0 -> { // Fraction contains just a fraction (no whole number). @@ -86,6 +87,6 @@ operator fun Fraction.unaryMinus(): Fraction { } /** Returns the greatest common divisor between two integers. */ -fun gcd(x: Int, y: Int): Int { +private fun gcd(x: Int, y: Int): Int { return if (y == 0) x else gcd(y, x % y) } diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index a4ba72213be..25bb5f2955d 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -17,6 +17,12 @@ fun Polynomial.isConstant(): Boolean = termCount == 1 && getTerm(0).variableCoun */ fun Polynomial.getConstant(): Real = getTerm(0).coefficient +/** + * Returns a human-readable, plaintext representation of this [Polynomial]. + * + * The returned string is guaranteed to be a syntactically correct algebraic expression representing + * the polynomial, e.g. "1+x-7x^2"). + */ fun Polynomial.toPlainText(): String { return termList.map { it.toPlainText() }.reduce { acc, termAnswerStr -> if (termAnswerStr.startsWith("-")) { diff --git a/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt index 83d85e9098c..4b2b559d579 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt @@ -21,3 +21,8 @@ fun RatioExpression.toSimplestForm(): List { fun RatioExpression.toAnswerString(): String { return ratioComponentList.joinToString(separator = ":") } + +/** Returns the greatest common divisor between two integers. */ +private fun gcd(x: Int, y: Int): Int { + return if (y == 0) x else gcd(y, x % y) +} diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 6df36abd3b6..b4fb0a39dad 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -6,24 +6,42 @@ import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET +/** + * Returns whether this [Real] is explicitly a rational type (i.e. a fraction). + * + * This returns false if the real is an integer despite that being mathematically rational. + */ fun Real.isRational(): Boolean = realTypeCase == RATIONAL +/** Returns whether this [Real] is negative. */ fun Real.isNegative(): Boolean = when (realTypeCase) { RATIONAL -> rational.isNegative IRRATIONAL -> irrational < 0 INTEGER -> integer < 0 - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $this.") } +/** + * Returns a [Double] representation of this [Real] that is approximately the same value (per + * [isApproximatelyEqualTo]). + */ fun Real.toDouble(): Double { return when (realTypeCase) { RATIONAL -> rational.toDouble() INTEGER -> integer.toDouble() IRRATIONAL -> irrational - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $this.") } } +/** + * Returns a human-readable, plaintext representation of this [Real]. + * + * Note that the returned value is guaranteed to be a self-contained numeric expression representing + * the real (which means proper fractions are converted to improper answer strings since fractions + * like '1 1/2' can't be written as a numeric expression without converting them to an improper + * form: '3/2'). + */ fun Real.toPlainText(): String = when (realTypeCase) { // Note that the rational part is first converted to an improper fraction since mixed fractions // can't be expressed as a single coefficient in typical polynomial syntax). @@ -33,19 +51,32 @@ fun Real.toPlainText(): String = when (realTypeCase) { REALTYPE_NOT_SET, null -> "" } +/** + * Returns whether this [Real] is approximately equal to the specified [Double] per + * [Double.approximatelyEquals]. + */ fun Real.isApproximatelyEqualTo(value: Double): Boolean { return toDouble().approximatelyEquals(value) } +/** + * Returns a negative version of this [Real] such that the original real plus the negative version + * would result in zero. + */ operator fun Real.unaryMinus(): Real { return when (realTypeCase) { RATIONAL -> recompute { it.setRational(-rational) } IRRATIONAL -> recompute { it.setIrrational(-irrational) } INTEGER -> recompute { it.setInteger(-integer) } - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $this.") } } +/** + * Returns an absolute value of this [Real] (that is, a non-negative [Real]). + * + * [isNegative] is guaranteed to return false for the returned value. + */ fun abs(real: Real): Real = if (real.isNegative()) -real else real private fun Real.recompute(transform: (Real.Builder) -> Real.Builder): Real { 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 index 313a5a1f751..f85a7664379 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -4,6 +4,60 @@ Tests for general-purpose mathematics utilities. load("//:oppia_android_test.bzl", "oppia_android_test") +oppia_android_test( + name = "FloatExtensionsTest", + srcs = ["FloatExtensionsTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.FloatExtensionsTest", + 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", + ], +) + +oppia_android_test( + name = "FractionExtensionsTest", + srcs = ["FractionExtensionsTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.FractionExtensionsTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/math:fraction_subject", + "//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", + ], +) + +oppia_android_test( + name = "PolynomialExtensionsTest", + srcs = ["PolynomialExtensionsTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.PolynomialExtensionsTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", + "//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", + ], +) + oppia_android_test( name = "RatioExtensionsTest", srcs = ["RatioExtensionsTest.kt"], @@ -20,3 +74,22 @@ oppia_android_test( "//utility/src/main/java/org/oppia/android/util/math:extensions", ], ) + +oppia_android_test( + name = "RealExtensionsTest", + srcs = ["RealExtensionsTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.RealExtensionsTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/math:real_subject", + "//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/FloatExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt new file mode 100644 index 00000000000..9010828169e --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt @@ -0,0 +1,155 @@ +package org.oppia.android.util.math + +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.robolectric.annotation.LooperMode + +/** Tests for [Float] and [Double] extensions. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class FloatExtensionsTest { + + @Test + fun testFloat_approximatelyEquals_bothZero_returnsTrue() { + val leftFloat = 0f + val rightFloat = 0f + + val result = leftFloat.approximatelyEquals(rightFloat) + + assertThat(result).isTrue() + } + + @Test + fun testFloat_approximatelyEquals_sameNonZeroValue_returnsTrue() { + val leftFloat = 1.2f + val rightFloat = 1.2f + + val result = leftFloat.approximatelyEquals(rightFloat) + + assertThat(result).isTrue() + } + + @Test + fun testFloat_approximatelyEquals_nonZeroValues_withinInterval_returnsTrue() { + val leftFloat = 1.2f + val rightFloat = leftFloat + FLOAT_EQUALITY_INTERVAL.toFloat() / 10f + + val result = leftFloat.approximatelyEquals(rightFloat) + + // Verify that they are approximately equal, but not actually the same float. + assertThat(result).isTrue() + assertThat(leftFloat).isNotEqualTo(rightFloat) + } + + @Test + fun testFloat_approximatelyEquals_nonZeroValues_outsideInterval_returnsFalse() { + val leftFloat = 1.2f + val rightFloat = leftFloat + FLOAT_EQUALITY_INTERVAL.toFloat() * 2f + + val result = leftFloat.approximatelyEquals(rightFloat) + + assertThat(result).isFalse() + } + + @Test + fun testFloat_approximatelyEquals_nonZeroValues_veryDifferent_returnsFalse() { + val leftFloat = 1.2f + val rightFloat = 7.3f + + val result = leftFloat.approximatelyEquals(rightFloat) + + assertThat(result).isFalse() + } + + @Test + fun testDouble_approximatelyEquals_bothZero_returnsTrue() { + val leftDouble = 0.0 + val rightDouble = 0.0 + + val result = leftDouble.approximatelyEquals(rightDouble) + + assertThat(result).isTrue() + } + + @Test + fun testDouble_approximatelyEquals_sameNonZeroValue_returnsTrue() { + val leftDouble = 1.2 + val rightDouble = 1.2 + + val result = leftDouble.approximatelyEquals(rightDouble) + + assertThat(result).isTrue() + } + + @Test + fun testDouble_approximatelyEquals_nonZeroValues_withinInterval_returnsTrue() { + val leftDouble = 1.2 + val rightDouble = leftDouble + FLOAT_EQUALITY_INTERVAL / 10.0 + + val result = leftDouble.approximatelyEquals(rightDouble) + + // Verify that they are approximately equal, but not actually the same double. + assertThat(result).isTrue() + assertThat(leftDouble).isNotEqualTo(rightDouble) + } + + @Test + fun testDouble_approximatelyEquals_nonZeroValues_outsideInterval_returnsFalse() { + val leftDouble = 1.2 + val rightDouble = leftDouble + FLOAT_EQUALITY_INTERVAL * 2 + + val result = leftDouble.approximatelyEquals(rightDouble) + + assertThat(result).isFalse() + } + + @Test + fun testDouble_approximatelyEquals_nonZeroValues_veryDifferent_returnsFalse() { + val leftDouble = 1.2 + val rightDouble = 7.3 + + val result = leftDouble.approximatelyEquals(rightDouble) + + assertThat(result).isFalse() + } + + @Test + fun testDouble_toPlainText_zero_returnsStringWithZero() { + val testDouble = 0.0 + + val plainText = testDouble.toPlainString() + + assertThat(plainText).isEqualTo("0.0") + } + + @Test + fun testDouble_toPlainText_nonZero_returnsStringForNonZero() { + val testDouble = 4.0 + + val plainText = testDouble.toPlainString() + + assertThat(plainText).isEqualTo("4.0") + } + + @Test + fun testDouble_toPlainText_negativeMultiDigitNumber_returnsCorrectString() { + val testDouble = -1.73 + + val plainText = testDouble.toPlainString() + + assertThat(plainText).isEqualTo("-1.73") + } + + @Test + fun testDouble_toPlainText_largeNumber_returnsNumberWithoutScientificNotation() { + val testDouble = 84758123.3213989 + + val plainText = testDouble.toPlainString() + + assertThat(plainText).isEqualTo("84758123.3213989") + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/FractionExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/FractionExtensionsTest.kt new file mode 100644 index 00000000000..48121da69d7 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/FractionExtensionsTest.kt @@ -0,0 +1,530 @@ +package org.oppia.android.util.math + +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.app.model.Fraction +import org.oppia.android.testing.assertThrows +import org.oppia.android.testing.math.FractionSubject.Companion.assertThat +import org.robolectric.annotation.LooperMode +import java.lang.ArithmeticException + +/** Tests for [Fraction] extensions. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class FractionExtensionsTest { + private companion object { + private val ZERO_FRACTION = Fraction.newBuilder().apply { + denominator = 1 + }.build() + + private val NEGATIVE_ZERO_FRACTION = Fraction.newBuilder().apply { + isNegative = true + denominator = 1 + }.build() + + private val ONE_HALF_FRACTION = Fraction.newBuilder().apply { + numerator = 1 + denominator = 2 + }.build() + + private val NEGATIVE_ONE_HALF_FRACTION = Fraction.newBuilder().apply { + isNegative = true + numerator = 1 + denominator = 2 + }.build() + + private val ONE_AND_ONE_HALF_FRACTION = Fraction.newBuilder().apply { + wholeNumber = 1 + numerator = 1 + denominator = 2 + }.build() + + private val NEGATIVE_ONE_AND_ONE_HALF_FRACTION = Fraction.newBuilder().apply { + isNegative = true + wholeNumber = 1 + numerator = 1 + denominator = 2 + }.build() + + private val THREE_HALVES_FRACTION = Fraction.newBuilder().apply { + numerator = 3 + denominator = 2 + }.build() + + private val NEGATIVE_THREE_HALVES_FRACTION = Fraction.newBuilder().apply { + isNegative = true + numerator = 3 + denominator = 2 + }.build() + + private val THREE_ONES_FRACTION = Fraction.newBuilder().apply { + numerator = 3 + denominator = 1 + }.build() + + private val NEGATIVE_THREE_ONES_FRACTION = Fraction.newBuilder().apply { + isNegative = true + numerator = 3 + denominator = 1 + }.build() + + private val TWO_FRACTION = Fraction.newBuilder().apply { + wholeNumber = 2 + denominator = 1 + }.build() + + private val NEGATIVE_TWO_FRACTION = Fraction.newBuilder().apply { + isNegative = true + wholeNumber = 2 + denominator = 1 + }.build() + } + + @Test + fun testHasFractionalPart_zeroFraction_returnsFalse() { + val result = ZERO_FRACTION.hasFractionalPart() + + assertThat(result).isFalse() + } + + @Test + fun testHasFractionalPart_oneHalf_returnsTrue() { + val result = ONE_HALF_FRACTION.hasFractionalPart() + + assertThat(result).isTrue() + } + + @Test + fun testHasFractionalPart_negativeOneHalf_returnsTrue() { + val result = NEGATIVE_ONE_HALF_FRACTION.hasFractionalPart() + + assertThat(result).isTrue() + } + + @Test + fun testHasFractionalPart_mixedFraction_returnsTrue() { + val result = ONE_AND_ONE_HALF_FRACTION.hasFractionalPart() + + assertThat(result).isTrue() + } + + @Test + fun testHasFractionalPart_improperFraction_returnsTrue() { + val result = THREE_HALVES_FRACTION.hasFractionalPart() + + assertThat(result).isTrue() + } + + @Test + fun testHasFractionalPart_threeOverOne_returnsTrue() { + val result = THREE_ONES_FRACTION.hasFractionalPart() + + assertThat(result).isTrue() + } + + @Test + fun testHasFractionalPart_onlyWholeNumber_returnsFalse() { + val result = TWO_FRACTION.hasFractionalPart() + + assertThat(result).isFalse() + } + + @Test + fun testIsOnlyWholeNumber_zeroFraction_returnsTrue() { + val result = ZERO_FRACTION.isOnlyWholeNumber() + + assertThat(result).isTrue() + } + + @Test + fun testIsOnlyWholeNumber_oneHalf_returnsFalse() { + val result = ONE_HALF_FRACTION.isOnlyWholeNumber() + + assertThat(result).isFalse() + } + + @Test + fun testIsOnlyWholeNumber_negativeOneHalf_returnsFalse() { + val result = NEGATIVE_ONE_HALF_FRACTION.isOnlyWholeNumber() + + assertThat(result).isFalse() + } + + @Test + fun testIsOnlyWholeNumber_mixedFraction_returnsFalse() { + val result = ONE_AND_ONE_HALF_FRACTION.isOnlyWholeNumber() + + assertThat(result).isFalse() + } + + @Test + fun testIsOnlyWholeNumber_improperFraction_returnsFalse() { + val result = THREE_HALVES_FRACTION.isOnlyWholeNumber() + + assertThat(result).isFalse() + } + + @Test + fun testIsOnlyWholeNumber_threeOverOne_returnsFalse() { + val result = THREE_ONES_FRACTION.isOnlyWholeNumber() + + // 3/1 is technically not a whole number since it's still in fractional form. + assertThat(result).isFalse() + } + + @Test + fun testIsOnlyWholeNumber_onlyWholeNumber_returnsTrue() { + val result = TWO_FRACTION.isOnlyWholeNumber() + + assertThat(result).isTrue() + } + + @Test + fun testToDouble_zeroFraction_returnsZero() { + val result = ZERO_FRACTION.toDouble() + + assertThat(result).isWithin(1e-5).of(0.0) + } + + @Test + fun testToDouble_oneHalf_returnsPointFive() { + val result = ONE_HALF_FRACTION.toDouble() + + assertThat(result).isWithin(1e-5).of(0.5) + } + + @Test + fun testToDouble_negativeOneHalf_returnsNegativePointFive() { + val result = NEGATIVE_ONE_HALF_FRACTION.toDouble() + + assertThat(result).isWithin(1e-5).of(-0.5) + } + + @Test + fun testToDouble_one_and_one_half_returnsOnePointFive() { + val result = ONE_AND_ONE_HALF_FRACTION.toDouble() + + assertThat(result).isWithin(1e-5).of(1.5) + } + + @Test + fun testToDouble_threeHalves_returnsOnePointFive() { + val result = THREE_HALVES_FRACTION.toDouble() + + assertThat(result).isWithin(1e-5).of(1.5) + } + + @Test + fun testToDouble_two_returnsTwo() { + val result = TWO_FRACTION.toDouble() + + assertThat(result).isWithin(1e-5).of(2.0) + } + + @Test + fun testToAnswerString_zero_returnsZeroString() { + val result = ZERO_FRACTION.toAnswerString() + + assertThat(result).isEqualTo("0") + } + + @Test + fun testToAnswerString_negativeZero_returnsZeroString() { + val result = NEGATIVE_ZERO_FRACTION.toAnswerString() + + assertThat(result).isEqualTo("0") + } + + @Test + fun testToAnswerString_two_returnsTwoString() { + val result = TWO_FRACTION.toAnswerString() + + assertThat(result).isEqualTo("2") + } + + @Test + fun testToAnswerString_negativeTwo_returnsMinusTwoString() { + val result = NEGATIVE_TWO_FRACTION.toAnswerString() + + assertThat(result).isEqualTo("-2") + } + + @Test + fun testToAnswerString_threeOverOne_returnsThreeString() { + val result = THREE_ONES_FRACTION.toAnswerString() + + assertThat(result).isEqualTo("3") + } + + @Test + fun testToAnswerString_negativeThreeOverOne_returnsMinusThreeString() { + val result = NEGATIVE_THREE_ONES_FRACTION.toAnswerString() + + assertThat(result).isEqualTo("-3") + } + + @Test + fun testToAnswerString_threeOverTwo_returnsThreeHalvesString() { + val result = THREE_HALVES_FRACTION.toAnswerString() + + assertThat(result).isEqualTo("3/2") + } + + @Test + fun testToAnswerString_negativeThreeOverTwo_returnsMinusThreeHalvesString() { + val result = NEGATIVE_THREE_HALVES_FRACTION.toAnswerString() + + assertThat(result).isEqualTo("-3/2") + } + + @Test + fun testToAnswerString_oneAndOneHalf_returnsMixedFractionString() { + val result = ONE_AND_ONE_HALF_FRACTION.toAnswerString() + + assertThat(result).isEqualTo("1 1/2") + } + + @Test + fun testToAnswerString_negativeOneAndOneHalf_returnsMinusMixedFractionString() { + val result = NEGATIVE_ONE_AND_ONE_HALF_FRACTION.toAnswerString() + + assertThat(result).isEqualTo("-1 1/2") + } + + @Test + fun testToSimplestForm_zero_returnsZeroFraction() { + val result = ZERO_FRACTION.toSimplestForm() + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testToSimplestForm_two_returnsTwoFraction() { + val result = TWO_FRACTION.toSimplestForm() + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(2) + } + + @Test + fun testToSimplestForm_oneHalf_returnsOneHalfFraction() { + val result = ONE_HALF_FRACTION.toSimplestForm() + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(2) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testToSimplestForm_oneAndOneHalf_returnsOneAndOneHalfFraction() { + val result = ONE_AND_ONE_HALF_FRACTION.toSimplestForm() + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(2) + assertThat(result).hasWholeNumberThat().isEqualTo(1) + } + + @Test + fun testToSimplestForm_sixFourths_returnsThreeHalvesFraction() { + val sixHalvesFraction = Fraction.newBuilder().apply { + numerator = 6 + denominator = 4 + }.build() + + val result = sixHalvesFraction.toSimplestForm() + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(3) + assertThat(result).hasDenominatorThat().isEqualTo(2) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testToSimplestForm_largeNegativeImproperFraction_reducesToSimplestImproperFraction() { + val largeImproperFraction = Fraction.newBuilder().apply { + isNegative = true + numerator = 1650 + denominator = 209 + }.build() + + val result = largeImproperFraction.toSimplestForm() + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(150) + assertThat(result).hasDenominatorThat().isEqualTo(19) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testToSimplestForm_zeroDenominator_throwsException() { + val zeroDenominatorFraction = Fraction.getDefaultInstance() + + // Converting to simplest form results in a divide by zero in this case. + assertThrows(ArithmeticException::class) { zeroDenominatorFraction.toSimplestForm() } + } + + @Test + fun testToImproperForm_zero_returnsZeroFraction() { + val result = ZERO_FRACTION.toImproperForm() + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testToImproperForm_two_returnsTwoOnesFraction() { + val result = TWO_FRACTION.toImproperForm() + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(2) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testToImproperForm_oneHalf_returnsOneHalfFraction() { + val result = ONE_HALF_FRACTION.toImproperForm() + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(2) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testToImproperForm_oneAndOneHalf_returnsThreeHalvesFraction() { + val result = ONE_AND_ONE_HALF_FRACTION.toImproperForm() + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(3) + assertThat(result).hasDenominatorThat().isEqualTo(2) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testToImproperForm_threeHalves_returnsThreeHalvesFraction() { + val result = THREE_HALVES_FRACTION.toImproperForm() + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(3) + assertThat(result).hasDenominatorThat().isEqualTo(2) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testToImproperForm_negativeOneAndTwoThirds_returnsNegativeFiveThirdsFraction() { + val negativeOneAndTwoThirds = Fraction.newBuilder().apply { + isNegative = true + numerator = 2 + denominator = 3 + wholeNumber = 1 + }.build() + + val result = negativeOneAndTwoThirds.toImproperForm() + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(5) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testToImproperForm_largeSimpleFormFraction_returnsLargeImproperFraction() { + val negativeOneAndTwoThirds = Fraction.newBuilder().apply { + isNegative = true + numerator = 17 + denominator = 19 + wholeNumber = 7 + }.build() + + val result = negativeOneAndTwoThirds.toImproperForm() + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(150) + assertThat(result).hasDenominatorThat().isEqualTo(19) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testUnaryMinus_zero_returnsNegativeZeroFraction() { + val result = -ZERO_FRACTION + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testUnaryMinus_two_returnsNegativeTwoFraction() { + val result = -TWO_FRACTION + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(2) + } + + @Test + fun testUnaryMinus_negativeTwo_returnsTwoFraction() { + val result = -NEGATIVE_TWO_FRACTION + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(2) + } + + @Test + fun testUnaryMinus_oneHalf_returnsNegativeOneHalfFraction() { + val result = -ONE_HALF_FRACTION + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(2) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testUnaryMinus_negativeOneHalf_returnsOneHalfFraction() { + val result = -NEGATIVE_ONE_HALF_FRACTION + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(2) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testUnaryMinus_oneAndOneHalf_returnsNegativeOneAndOneHalfFraction() { + val result = -ONE_AND_ONE_HALF_FRACTION + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(2) + assertThat(result).hasWholeNumberThat().isEqualTo(1) + } + + @Test + fun testUnaryMinus_negativeOneAndOneHalf_returnsOneAndOneHalfFraction() { + val result = -NEGATIVE_ONE_AND_ONE_HALF_FRACTION + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(2) + assertThat(result).hasWholeNumberThat().isEqualTo(1) + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt new file mode 100644 index 00000000000..4fac2ae26b0 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt @@ -0,0 +1,390 @@ +package org.oppia.android.util.math + +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.app.model.Fraction +import org.oppia.android.app.model.Polynomial +import org.oppia.android.app.model.Polynomial.Term +import org.oppia.android.app.model.Polynomial.Term.Variable +import org.oppia.android.app.model.Real +import org.oppia.android.testing.math.RealSubject.Companion.assertThat +import org.robolectric.annotation.LooperMode + +/** Tests for [Polynomial] extensions. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class PolynomialExtensionsTest { + private companion object { + private const val PI = 3.1415 + + private val ONE_HALF_FRACTION = Fraction.newBuilder().apply { + numerator = 1 + denominator = 2 + }.build() + + private val ONE_AND_ONE_HALF_FRACTION = Fraction.newBuilder().apply { + numerator = 1 + denominator = 2 + wholeNumber = 1 + }.build() + + private val ZERO_REAL = Real.newBuilder().apply { + integer = 0 + }.build() + + private val ONE_REAL = Real.newBuilder().apply { + integer = 1 + }.build() + + private val TWO_REAL = Real.newBuilder().apply { + integer = 2 + }.build() + + private val ONE_HALF_REAL = Real.newBuilder().apply { + rational = ONE_HALF_FRACTION + }.build() + + private val ONE_AND_ONE_HALF_REAL = Real.newBuilder().apply { + rational = ONE_AND_ONE_HALF_FRACTION + }.build() + + private val PI_REAL = Real.newBuilder().apply { + irrational = PI + }.build() + + private val ZERO_POLYNOMIAL = createPolynomial(createTerm(coefficient = ZERO_REAL)) + + private val TWO_POLYNOMIAL = createPolynomial(createTerm(coefficient = TWO_REAL)) + + private val NEGATIVE_TWO_POLYNOMIAL = createPolynomial(createTerm(coefficient = -TWO_REAL)) + + private val ONE_HALF_POLYNOMIAL = createPolynomial(createTerm(coefficient = ONE_HALF_REAL)) + + private val NEGATIVE_ONE_HALF_POLYNOMIAL = + createPolynomial(createTerm(coefficient = -ONE_HALF_REAL)) + + private val ONE_AND_ONE_HALF_POLYNOMIAL = + createPolynomial(createTerm(coefficient = ONE_AND_ONE_HALF_REAL)) + + private val NEGATIVE_ONE_AND_ONE_HALF_POLYNOMIAL = + createPolynomial(createTerm(coefficient = -ONE_AND_ONE_HALF_REAL)) + + private val PI_POLYNOMIAL = createPolynomial(createTerm(coefficient = PI_REAL)) + + private val NEGATIVE_PI_POLYNOMIAL = createPolynomial(createTerm(coefficient = -PI_REAL)) + + private val ONE_X_POLYNOMIAL = + createPolynomial(createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 1))) + + private val NEGATIVE_ONE_X_POLYNOMIAL = + createPolynomial(createTerm(coefficient = -ONE_REAL, createVariable(name = "x", power = 1))) + + private val TWO_X_POLYNOMIAL = + createPolynomial(createTerm(coefficient = TWO_REAL, createVariable(name = "x", power = 1))) + + private val ONE_PLUS_X_POLYNOMIAL = + createPolynomial( + createTerm(coefficient = ONE_REAL), + createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 1)) + ) + } + + @Test + fun testIsConstant_default_returnsFalse() { + val defaultPolynomial = Polynomial.getDefaultInstance() + + val result = defaultPolynomial.isConstant() + + assertThat(result).isFalse() + } + + @Test + fun testIsConstant_zero_returnsTrue() { + val result = ZERO_POLYNOMIAL.isConstant() + + assertThat(result).isTrue() + } + + @Test + fun testIsConstant_two_returnsTrue() { + val result = TWO_POLYNOMIAL.isConstant() + + assertThat(result).isTrue() + } + + @Test + fun testIsConstant_negativeTwo_returnsTrue() { + val result = NEGATIVE_TWO_POLYNOMIAL.isConstant() + + assertThat(result).isTrue() + } + + @Test + fun testIsConstant_oneHalf_returnsTrue() { + val result = ONE_HALF_POLYNOMIAL.isConstant() + + assertThat(result).isTrue() + } + + @Test + fun testIsConstant_negativeOneHalf_returnsTrue() { + val result = NEGATIVE_ONE_HALF_POLYNOMIAL.isConstant() + + assertThat(result).isTrue() + } + + @Test + fun testIsConstant_pi_returnsTrue() { + val result = PI_POLYNOMIAL.isConstant() + + assertThat(result).isTrue() + } + + @Test + fun testIsConstant_negativePi_returnsTrue() { + val result = NEGATIVE_PI_POLYNOMIAL.isConstant() + + assertThat(result).isTrue() + } + + @Test + fun testIsConstant_x_returnsFalse() { + val result = ONE_X_POLYNOMIAL.isConstant() + + assertThat(result).isFalse() + } + + @Test + fun testIsConstant_2x_returnsFalse() { + val result = TWO_X_POLYNOMIAL.isConstant() + + assertThat(result).isFalse() + } + + @Test + fun testIsConstant_one_and_x_returnsFalse() { + val result = ONE_PLUS_X_POLYNOMIAL.isConstant() + + assertThat(result).isFalse() + } + + @Test + fun testIsConstant_one_and_two_returnsFalse() { + val onePlusTwoPolynomial = + createPolynomial(createTerm(coefficient = ONE_REAL), createTerm(coefficient = TWO_REAL)) + + val result = onePlusTwoPolynomial.isConstant() + + // While 1+2 is effectively a constant polynomial, it's not actually simplified and thus isn't + // considered a constant polynomial. + assertThat(result).isFalse() + } + + @Test + fun testGetConstant_zero_returnsZero() { + val result = ZERO_POLYNOMIAL.getConstant() + + assertThat(result).isIntegerThat().isEqualTo(0) + } + + @Test + fun testGetConstant_two_returnsTwo() { + val result = TWO_POLYNOMIAL.getConstant() + + assertThat(result).isIntegerThat().isEqualTo(2) + } + + @Test + fun testGetConstant_negativeTwo_returnsNegativeTwo() { + val result = NEGATIVE_TWO_POLYNOMIAL.getConstant() + + assertThat(result).isIntegerThat().isEqualTo(-2) + } + + @Test + fun testGetConstant_oneHalf_returnsOneHalf() { + val result = ONE_HALF_POLYNOMIAL.getConstant() + + assertThat(result).isRationalThat().evaluatesToDoubleThat().isWithin(1e-5).of(0.5) + } + + @Test + fun testGetConstant_negativeOneHalf_returnsNegativeOneHalf() { + val result = NEGATIVE_ONE_HALF_POLYNOMIAL.getConstant() + + assertThat(result).isRationalThat().evaluatesToDoubleThat().isWithin(1e-5).of(-0.5) + } + + @Test + fun testGetConstant_pi_returnsPi() { + val result = PI_POLYNOMIAL.getConstant() + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(PI) + } + + @Test + fun testGetConstant_negativePi_returnsNegativePi() { + val result = NEGATIVE_PI_POLYNOMIAL.getConstant() + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(-PI) + } + + @Test + fun testToPlainText_zero_returnsZeroString() { + val result = ZERO_POLYNOMIAL.toPlainText() + + assertThat(result).isEqualTo("0") + } + + @Test + fun testToPlainText_two_returnsTwoString() { + val result = TWO_POLYNOMIAL.toPlainText() + + assertThat(result).isEqualTo("2") + } + + @Test + fun testToPlainText_negativeTwo_returnsMinusTwoString() { + val result = NEGATIVE_TWO_POLYNOMIAL.toPlainText() + + assertThat(result).isEqualTo("-2") + } + + @Test + fun testToPlainText_oneAndOneHalf_returnsThreeHalvesString() { + val result = ONE_AND_ONE_HALF_POLYNOMIAL.toPlainText() + + assertThat(result).isEqualTo("3/2") + } + + @Test + fun testToPlainText_negativeOneAndOneHalf_returnsMinusThreeHalvesString() { + val result = NEGATIVE_ONE_AND_ONE_HALF_POLYNOMIAL.toPlainText() + + assertThat(result).isEqualTo("-3/2") + } + + @Test + fun testToPlainText_pi_returnsPiString() { + val result = PI_POLYNOMIAL.toPlainText() + + assertThat(result).isEqualTo("3.1415") + } + + @Test + fun testToPlainText_negativePi_returnsMinusPiString() { + val result = NEGATIVE_PI_POLYNOMIAL.toPlainText() + + assertThat(result).isEqualTo("-3.1415") + } + + @Test + fun testToPlainText_2x_returns2XString() { + val result = TWO_X_POLYNOMIAL.toPlainText() + + assertThat(result).isEqualTo("2x") + } + + @Test + fun testToPlainText_1x_returnsXString() { + val result = ONE_X_POLYNOMIAL.toPlainText() + + assertThat(result).isEqualTo("x") + } + + @Test + fun testToPlainText_negativeX_returnsMinusXString() { + val result = NEGATIVE_ONE_X_POLYNOMIAL.toPlainText() + + assertThat(result).isEqualTo("-x") + } + + @Test + fun testToPlainText_oneAndX_returnsOnePlusXString() { + val result = ONE_PLUS_X_POLYNOMIAL.toPlainText() + + assertThat(result).isEqualTo("1 + x") + } + + @Test + fun testToPlainText_oneAndNegativeX_returnsOneMinusXString() { + val oneMinusXPolynomial = createPolynomial( + createTerm(coefficient = ONE_REAL), + createTerm(coefficient = -ONE_REAL, createVariable(name = "x", power = 1)) + ) + + val result = oneMinusXPolynomial.toPlainText() + + assertThat(result).isEqualTo("1 - x") + } + + @Test + fun testToPlainText_oneAndOneHalfXAndY_returnsThreeHalvesXPlusYString() { + val oneMinusXPolynomial = createPolynomial( + createTerm(coefficient = ONE_AND_ONE_HALF_REAL, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE_REAL, createVariable(name = "y", power = 1)) + ) + + val result = oneMinusXPolynomial.toPlainText() + + assertThat(result).isEqualTo("(3/2)x + y") + } + + @Test + fun testToPlainText_oneAndXAndXSquared_returnsOnePlusXPlusXSquaredString() { + val oneMinusXPolynomial = createPolynomial( + createTerm(coefficient = ONE_REAL), + createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 2)) + ) + + val result = oneMinusXPolynomial.toPlainText() + + assertThat(result).isEqualTo("1 + x + x^2") + } + + @Test + fun testToPlainText_xSquaredAndXAndOne_returnsXSquaredPlusXPlusOneString() { + val oneMinusXPolynomial = createPolynomial( + createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 2)), + createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE_REAL) + ) + + val result = oneMinusXPolynomial.toPlainText() + + // Compared with the test above, this shows that term order matters for string conversion. + assertThat(result).isEqualTo("x^2 + x + 1") + } + + @Test + fun testToPlainText_xSquaredYCubedAndOne_returnsXSquaredYCubedPlusOneString() { + val oneMinusXPolynomial = createPolynomial( + createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 2)), + createTerm(coefficient = ONE_REAL, createVariable(name = "y", power = 3)), + createTerm(coefficient = ONE_REAL) + ) + + val result = oneMinusXPolynomial.toPlainText() + + assertThat(result).isEqualTo("x^2 + y^3 + 1") + } +} + +private fun createVariable(name: String, power: Int) = Variable.newBuilder().apply { + this.name = name + this.power = power +}.build() + +private fun createTerm(coefficient: Real, vararg variables: Variable) = Term.newBuilder().apply { + this.coefficient = coefficient + addAllVariable(variables.toList()) +}.build() + +private fun createPolynomial(vararg terms: Term) = Polynomial.newBuilder().apply { + addAllTerm(terms.toList()) +}.build() diff --git a/utility/src/test/java/org/oppia/android/util/math/RatioExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RatioExtensionsTest.kt index ca380220b0b..2b9bea5df06 100644 --- a/utility/src/test/java/org/oppia/android/util/math/RatioExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RatioExtensionsTest.kt @@ -7,7 +7,9 @@ import org.junit.runner.RunWith import org.oppia.android.app.model.RatioExpression import org.robolectric.annotation.LooperMode -/** Tests for [RatioExtensions]. */ +/** Tests for [RatioExpression] extensions. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class RatioExtensionsTest { diff --git a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt new file mode 100644 index 00000000000..2e13da959aa --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt @@ -0,0 +1,414 @@ +package org.oppia.android.util.math + +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.app.model.Fraction +import org.oppia.android.app.model.Real +import org.oppia.android.testing.assertThrows +import org.oppia.android.testing.math.RealSubject.Companion.assertThat +import org.robolectric.annotation.LooperMode + +/** Tests for [Real] extensions. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class RealExtensionsTest { + private companion object { + private const val PI = 3.1415 + + private val ONE_HALF_FRACTION = Fraction.newBuilder().apply { + numerator = 1 + denominator = 2 + }.build() + + private val ONE_AND_ONE_HALF_FRACTION = Fraction.newBuilder().apply { + numerator = 1 + denominator = 2 + wholeNumber = 1 + }.build() + + private val ZERO_REAL = createIntegerReal(0) + private val TWO_REAL = createIntegerReal(2) + private val NEGATIVE_TWO_REAL = createIntegerReal(-2) + + private val ONE_HALF_REAL = createRationalReal(ONE_HALF_FRACTION) + private val NEGATIVE_ONE_HALF_REAL = createRationalReal(-ONE_HALF_FRACTION) + private val ONE_AND_ONE_HALF_REAL = createRationalReal(ONE_AND_ONE_HALF_FRACTION) + private val NEGATIVE_ONE_AND_ONE_HALF_REAL = createRationalReal(-ONE_AND_ONE_HALF_FRACTION) + + private val PI_REAL = createIrrationalReal(PI) + private val NEGATIVE_PI_REAL = createIrrationalReal(-PI) + } + + @Test + fun testIsRational_default_returnsFalse() { + val defaultReal = Real.getDefaultInstance() + + val result = defaultReal.isRational() + + assertThat(result).isFalse() + } + + @Test + fun testIsRational_twoInteger_returnsFalse() { + val result = TWO_REAL.isRational() + + assertThat(result).isFalse() + } + + @Test + fun testIsRational_oneHalfFraction_returnsTrue() { + val result = ONE_HALF_REAL.isRational() + + assertThat(result).isTrue() + } + + @Test + fun testIsRational_piIrrational_returnsFalse() { + val result = PI_REAL.isRational() + + assertThat(result).isFalse() + } + + @Test + fun testIsNegative_default_throwsException() { + val defaultReal = Real.getDefaultInstance() + + val exception = assertThrows(IllegalStateException::class) { defaultReal.isNegative() } + + assertThat(exception).hasMessageThat().contains("Invalid real") + } + + @Test + fun testIsNegative_twoInteger_returnsFalse() { + val result = TWO_REAL.isNegative() + + assertThat(result).isFalse() + } + + @Test + fun testIsNegative_negativeTwoInteger_returnsTrue() { + val result = NEGATIVE_TWO_REAL.isNegative() + + assertThat(result).isTrue() + } + + @Test + fun testIsNegative_oneHalfFraction_returnsFalse() { + val result = ONE_HALF_REAL.isNegative() + + assertThat(result).isFalse() + } + + @Test + fun testIsNegative_negativeOneHalfFraction_returnsTrue() { + val result = NEGATIVE_ONE_HALF_REAL.isNegative() + + assertThat(result).isTrue() + } + + @Test + fun testIsNegative_piIrrational_returnsFalse() { + val result = PI_REAL.isNegative() + + assertThat(result).isFalse() + } + + @Test + fun testIsNegative_negativePiIrrational_returnsTrue() { + val result = NEGATIVE_PI_REAL.isNegative() + + assertThat(result).isTrue() + } + + @Test + fun testToDouble_default_returnsZeroDouble() { + val defaultReal = Real.getDefaultInstance() + + val exception = assertThrows(IllegalStateException::class) { defaultReal.toDouble() } + + assertThat(exception).hasMessageThat().contains("Invalid real") + } + + @Test + fun testToDouble_twoInteger_returnsTwoDouble() { + val result = TWO_REAL.toDouble() + + assertThat(result).isWithin(1e-5).of(2.0) + } + + @Test + fun testToDouble_negativeTwoInteger_returnsNegativeTwoDouble() { + val result = NEGATIVE_TWO_REAL.toDouble() + + assertThat(result).isWithin(1e-5).of(-2.0) + } + + @Test + fun testToDouble_oneHalfFraction_returnsPointFive() { + val result = ONE_HALF_REAL.toDouble() + + assertThat(result).isWithin(1e-5).of(0.5) + } + + @Test + fun testToDouble_negativeOneHalfFraction_returnsNegativePointFive() { + val result = NEGATIVE_ONE_HALF_REAL.toDouble() + + assertThat(result).isWithin(1e-5).of(-0.5) + } + + @Test + fun testToDouble_piIrrational_returnsPi() { + val result = PI_REAL.toDouble() + + assertThat(result).isWithin(1e-5).of(PI) + } + + @Test + fun testToDouble_negativePiIrrational_returnsNegativePi() { + val result = NEGATIVE_PI_REAL.toDouble() + + assertThat(result).isWithin(1e-5).of(-PI) + } + + @Test + fun testToPlainText_default_returnsEmptyString() { + val defaultReal = Real.getDefaultInstance() + + val result = defaultReal.toPlainText() + + assertThat(result).isEmpty() + } + + @Test + fun testToPlainText_twoInteger_returnsTwoString() { + val result = TWO_REAL.toPlainText() + + assertThat(result).isEqualTo("2") + } + + @Test + fun testToPlainText_negativeTwoInteger_returnsMinusTwoString() { + val result = NEGATIVE_TWO_REAL.toPlainText() + + assertThat(result).isEqualTo("-2") + } + + @Test + fun testToPlainText_oneHalfFraction_returnsOneHalfString() { + val result = ONE_HALF_REAL.toPlainText() + + assertThat(result).isEqualTo("1/2") + } + + @Test + fun testToPlainText_negativeOneHalfFraction_returnsMinusOneHalfString() { + val result = NEGATIVE_ONE_HALF_REAL.toPlainText() + + assertThat(result).isEqualTo("-1/2") + } + + @Test + fun testToPlainText_oneAndOneHalfFraction_returnsThreeHalvesString() { + val result = ONE_AND_ONE_HALF_REAL.toPlainText() + + assertThat(result).isEqualTo("3/2") + } + + @Test + fun testToPlainText_negativeOneAndOneHalfFraction_returnsMinusThreeHalvesString() { + val result = NEGATIVE_ONE_AND_ONE_HALF_REAL.toPlainText() + + assertThat(result).isEqualTo("-3/2") + } + + @Test + fun testToPlainText_piIrrational_returnsPiString() { + val result = PI_REAL.toPlainText() + + assertThat(result).isEqualTo("3.1415") + } + + @Test + fun testToPlainText_negativePiIrrational_returnsMinusPiString() { + val result = NEGATIVE_PI_REAL.toPlainText() + + assertThat(result).isEqualTo("-3.1415") + } + + @Test + fun testIsApproximatelyEqualTo_zeroIntegerAndZero_returnsTrue() { + val result = ZERO_REAL.isApproximatelyEqualTo(0.0) + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_zeroAndOne_returnsFalse() { + val result = ZERO_REAL.isApproximatelyEqualTo(1.0) + + assertThat(result).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_twoAndTwoWithinThreshold_returnsTrue() { + val result = TWO_REAL.isApproximatelyEqualTo(2.0 + FLOAT_EQUALITY_INTERVAL / 2.0) + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_twoAndTwoOutsideThreshold_returnsFalse() { + val result = TWO_REAL.isApproximatelyEqualTo(2.0 + FLOAT_EQUALITY_INTERVAL * 2.0) + + assertThat(result).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_oneHalfAndOne_returnsFalse() { + val result = ONE_HALF_REAL.isApproximatelyEqualTo(1.0) + + assertThat(result).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_oneHalfAndPointFive_returnsTrue() { + val result = ONE_HALF_REAL.isApproximatelyEqualTo(0.5) + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_oneHalfAndPointSix_returnsFalse() { + val result = ONE_HALF_REAL.isApproximatelyEqualTo(0.6) + + assertThat(result).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_pointFiveAndPointFive_returnsTrue() { + val pointFiveReal = createIrrationalReal(0.5) + + val result = pointFiveReal.isApproximatelyEqualTo(0.5) + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_pointFiveAndPointSix_returnsFalse() { + val pointFiveReal = createIrrationalReal(0.5) + + val result = pointFiveReal.isApproximatelyEqualTo(0.6) + + assertThat(result).isFalse() + } + + @Test + fun testUnaryMinus_default_throwsException() { + val defaultReal = Real.getDefaultInstance() + + val exception = assertThrows(IllegalStateException::class) { -defaultReal } + + assertThat(exception).hasMessageThat().contains("Invalid real") + } + + @Test + fun testUnaryMinus_twoInteger_returnsNegativeTwoInteger() { + val result = -TWO_REAL + + assertThat(result).isIntegerThat().isEqualTo(-2) + } + + @Test + fun testUnaryMinus_negativeTwoInteger_returnsTwoInteger() { + val result = -NEGATIVE_TWO_REAL + + assertThat(result).isIntegerThat().isEqualTo(2) + } + + @Test + fun testUnaryMinus_twoOneHalf_returnsNegativeOneHalf() { + val result = -ONE_HALF_REAL + + assertThat(result).isRationalThat().evaluatesToDoubleThat().isWithin(1e-5).of(-0.5) + } + + @Test + fun testUnaryMinus_negativeOneHalf_returnsOneHalf() { + val result = -NEGATIVE_ONE_HALF_REAL + + assertThat(result).isRationalThat().evaluatesToDoubleThat().isWithin(1e-5).of(0.5) + } + + @Test + fun testUnaryMinus_pi_returnsNegativePi() { + val result = -PI_REAL + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(-PI) + } + + @Test + fun testUnaryMinus_negativePi_returnsPi() { + val result = -NEGATIVE_PI_REAL + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(PI) + } + + @Test + fun testAbs_twoInteger_returnsTwoInteger() { + val result = abs(TWO_REAL) + + assertThat(result).isIntegerThat().isEqualTo(2) + } + + @Test + fun testAbs_negativeTwoInteger_returnsTwoInteger() { + val result = abs(NEGATIVE_TWO_REAL) + + assertThat(result).isIntegerThat().isEqualTo(2) + } + + @Test + fun testAbs_oneHalf_returnsOneHalf() { + val result = abs(ONE_HALF_REAL) + + assertThat(result).isRationalThat().evaluatesToDoubleThat().isWithin(1e-5).of(0.5) + } + + @Test + fun testAbs_negativeOneHalf_returnsOneHalf() { + val result = abs(NEGATIVE_ONE_HALF_REAL) + + assertThat(result).isRationalThat().evaluatesToDoubleThat().isWithin(1e-5).of(0.5) + } + + @Test + fun testAbs_pi_returnsPi() { + val result = abs(PI_REAL) + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(PI) + } + + @Test + fun testAbs_negativePi_returnsPi() { + val result = abs(NEGATIVE_PI_REAL) + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(PI) + } +} + +private fun createIntegerReal(value: Int) = Real.newBuilder().apply { + integer = value +}.build() + +private fun createRationalReal(value: Fraction) = Real.newBuilder().apply { + rational = value +}.build() + +private fun createIrrationalReal(value: Double) = Real.newBuilder().apply { + irrational = value +}.build() From fca0cd9ae3d5904812ed215d6816113f6888b06b Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 20 Jan 2022 22:22:44 -0800 Subject: [PATCH 046/134] Add parameterized test runner. This commit introduces a new parameterized test runner that allows proper combinations of parameterized & non-parameterized tests in the same suite, and in a way that should work on both Robolectric & Espresso (though the latter isn't currently verified). Further, this commit also introduces a TokenSubject that will be used more explicitly by the follow-up commit for verifying MathTokenizer. --- scripts/assets/test_file_exemptions.textproto | 8 + .../oppia/android/testing/junit/BUILD.bazel | 58 ++++ .../junit/OppiaParameterizedTestRunner.kt | 302 ++++++++++++++++++ .../android/testing/junit/ParameterValue.kt | 143 +++++++++ .../ParameterizedAndroidJUnit4ClassRunner.kt | 36 +++ .../testing/junit/ParameterizedMethod.kt | 44 +++ .../ParameterizedRobolectricTestRunner.kt | 49 +++ .../junit/ParameterizedRunnerDelegate.kt | 67 ++++ .../ParameterizedRunnerOverrideMethods.kt | 18 ++ .../oppia/android/testing/math/BUILD.bazel | 15 + .../android/testing/math/TokenSubject.kt | 173 ++++++++++ 11 files changed, 913 insertions(+) create mode 100644 testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/junit/ParameterizedMethod.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerOverrideMethods.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/math/TokenSubject.kt diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index ae6a4f367b2..e7b076af277 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -646,12 +646,20 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/Ge exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/ImageViewMatcher.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/KonfettiViewMatcher.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/DefineAppLanguageLocaleContext.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedMethod.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerOverrideMethods.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/TokenSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/mockito/MockitoKotlinHelper.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/network/ApiMockLoader.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/network/MockClassroomService.kt" diff --git a/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel index e8d62702d99..d7e6d99dd25 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel @@ -30,3 +30,61 @@ kt_android_library( "//third_party:junit_junit", ], ) + +kt_android_library( + name = "oppia_parameterized_test_runner", + testonly = True, + srcs = [ + "OppiaParameterizedTestRunner.kt", + ], + visibility = ["//:oppia_testing_visibility"], + deps = [ + ":parameterized_runner_delegate_impl", + "//third_party:androidx_test_runner", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + ], +) + +kt_android_library( + name = "parameterized_android_junit4_class_runner", + testonly = True, + srcs = [ + "ParameterizedAndroidJUnit4ClassRunner.kt", + ], + visibility = ["//:oppia_testing_visibility"], + deps = [ + ":parameterized_runner_delegate_impl", + "//third_party:androidx_test_runner", + "//third_party:junit_junit", + ], +) + +kt_android_library( + name = "parameterized_robolectric_test_runner", + testonly = True, + srcs = [ + "ParameterizedRobolectricTestRunner.kt", + ], + visibility = ["//:oppia_testing_visibility"], + deps = [ + ":parameterized_runner_delegate_impl", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + ], +) + +kt_android_library( + name = "parameterized_runner_delegate_impl", + testonly = True, + srcs = [ + "ParameterValue.kt", + "ParameterizedMethod.kt", + "ParameterizedRunnerDelegate.kt", + "ParameterizedRunnerOverrideMethods.kt", + ], + deps = [ + "//third_party:junit_junit", + ], +) diff --git a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt new file mode 100644 index 00000000000..3579bc95d5b --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt @@ -0,0 +1,302 @@ +package org.oppia.android.testing.junit + +import java.lang.annotation.Repeatable +import java.lang.reflect.Field +import java.lang.reflect.Method +import org.junit.runner.Description +import org.junit.runner.Runner +import org.junit.runner.manipulation.Filter +import org.junit.runner.manipulation.Filterable +import org.junit.runner.manipulation.Sortable +import org.junit.runner.manipulation.Sorter +import org.junit.runner.notification.RunNotifier +import org.junit.runners.Suite + +/** + * JUnit test runner that enables support for parameterization, that is, running a single test + * multiple times with different data values. + * + * From a testing correctness perspective, this should only be used to test scenarios of behaviors + * that are very similar to one another (i.e. only differ in one or two conditions that can be + * data-driven), and that have a large number (i.e. >5) conditions to test. For other situations, + * use regular explicit tests, instead (since parameterized tests can hurt test maintainability and + * readability). + * + * This runner behaves like AndroidJUnit4 in that it should work both locally (i.e. via Robolectric) + * and on a device (i.e. with Espresso), though the correct Bazel dependency needs to be added based + * on the environment in which the test is running. + * + * To introduce parameterized tests, add this runner along with one or more [Parameter]-annotated + * fields and one or more [RunParameterized]-annotated methods (where each method should have + * multiple [Iteration]s defined to describe each test iteration). Here's a simple example: + * + * ```kotlin + * @RunWith(OppiaParameterizedTestRunner::class) + * class ExampleParameterizedTest { + * @Parameter lateinit var parameter: String + * + * @Test + * @RunParameterized( + * Iteration("first", "parameter=first value"), + * Iteration("second", "parameter=second value"), + * Iteration("third", "parameter=third value") + * ) + * fun testParams_multipleVals_isConsistent() { + * val result = performOperation(parameter) + * assertThat(result).isEqualTo(consistentExpectedValue) + * } + * } + * ``` + * + * The test testParams_multipleVals_isConsistent will be run three times, and before each time the + * specified parameter value corresponding to each iteration will be injected into the parameter + * field for use by the test. + * + * Note that with Bazel specific iterations can be run by utilizing the test and iteration name, + * e.g.: + * + * ```bash + * bazel run //...:ExampleParameterizedTest --test_filter=testParams_multipleVals_isConsistent-first + * ``` + * + * Or, all of the iterations for that test can be run: + * + * ```bash + * bazel run //...:ExampleParameterizedTest --test_filter=testParams_multipleVals_isConsistent + * ``` + * + * Finally, regular tests can be added by simply using the JUnit ``Test`` annotation without also + * annotating with [RunParameterized]. Such tests should not ever read from the + * [Parameter]-annotated fields since there's no way to ensure what values those fields will + * contain (thus they should be treated as undefined outside of tests that specific define their + * value via [Iteration]). + */ +class OppiaParameterizedTestRunner(private val testClass: Class<*>): Suite(testClass, listOf()) { + private val parameterizedMethods = computeParameterizedMethods() + private val childrenRunners by lazy { + // Collect all parameterized methods (for each iteration they support) plus one test runner for + // all non-parameterized methods. + parameterizedMethods.flatMap { (methodName, method) -> + method.iterationNames.map { iterationName -> + ProxyParameterizedTestRunner(testClass, parameterizedMethods, methodName, iterationName) + } + } + ProxyParameterizedTestRunner(testClass, parameterizedMethods, methodName = null) + } + + override fun getChildren(): MutableList = childrenRunners.toMutableList() + + private fun computeParameterizedMethods(): Map { + val fieldsAndParsers = fetchParameterizedFields().map { field -> + val valueParser = ParameterValue.createParserForField(field) + checkNotNull(valueParser) { + "Unsupported field type: ${field.type} for parameterized field ${field.name}" + } + return@map field to valueParser + }.associateBy { (field, _) -> field.name } + + val fields = fieldsAndParsers.map { (_, fieldAndParser) -> fieldAndParser.first } + val methodDeclarations = fetchParameterizedMethodDeclarations() + return methodDeclarations.map { (method, rawValues) -> + val allValues = rawValues.mapValues { (_, values) -> + values.map { rawValuePair -> + check('=' in rawValuePair) { + "Expect all parameter values to be of the form propertyField=value (encountered:" + + " $rawValuePair)" + } + + val (fieldName, rawValue) = rawValuePair.split('=') + check(fieldName in fieldsAndParsers) { + "Property key does not correspond to any class fields: $fieldName (available:" + + " ${fieldsAndParsers.keys})" + } + + val (field, parser) = fieldsAndParsers.getValue(fieldName) + val value = parser.parseParameter(fieldName, rawValue) + checkNotNull(value) { + "Parameterized field ${field.name}'s type is incompatible with raw parameter value:" + + " $rawValue" + } + } + }.also { allValues -> + // Validate no duplicate keys. + allValues.forEach { (iterationName, values) -> + val allKeys = values.map { it.key } + val uniqueKeys = allKeys.toSet() + check(allKeys.size == uniqueKeys.size) { + val duplicateKeys = allKeys.toMutableList() + uniqueKeys.forEach { duplicateKeys.remove(it) } + return@check "Encountered duplicate keys in iteration $iterationName for method" + + " ${method.name}: ${duplicateKeys.toSet()}" + } + } + + // Validate key consistency. + val allKeys = allValues.values.flatten().map(ParameterValue::key).toSet() + allValues.forEach { (iterationName, values) -> + val iterationKeys = values.map { it.key }.toSet() + check(iterationKeys == allKeys) { + "Iteration $iterationName in method ${method.name} has missing keys compared with" + + " other iterations: ${allKeys - iterationKeys}" + } + } + + // Validate value ordering. + val iterationKeys = allValues.mapValues { (_, values) -> values.map { it.key } } + val expectedOrder = iterationKeys.values.first() + iterationKeys.forEach { (iterationName, keys) -> + check(keys == expectedOrder) { + "Iteration $iterationName in method ${method.name} lists its keys in the order: $keys" + + " whereas $expectedOrder (for the first iteration) is expected for consistency." + + " Please pick an order and ensure all iterations are consistent." + } + } + + // Validate that all value sets are unique (to detect redundant iterations). + allValues.entries.forEach { (outerIterationName, outerValues) -> + allValues.entries.forEach { (innerIterationName, innerValues) -> + if (outerIterationName != innerIterationName) { + // Order & counts have been verified above, so the values can be checked in order. + val differentValues = outerValues.zip(innerValues).any { (outerValue, innerValue) -> + outerValue.value != innerValue.value + } + check(differentValues) { + "Iterations $outerIterationName and $innerIterationName in method ${method.name}" + + " have the same values and are thus redundant. Please remove one of them or" + + " update the values." + } + } + } + } + } + return@map ParameterizedMethod(method.name, allValues, fields) + }.associateBy { it.methodName } + } + + private fun fetchParameterizedFields(): List { + return testClass.declaredFields.mapNotNull { field -> + field.getDeclaredAnnotation(Parameter::class.java)?.let { field } + } + } + + private fun fetchParameterizedMethodDeclarations(): List { + return testClass.declaredMethods.mapNotNull { method -> + method.getDeclaredAnnotationsByType(Iteration::class.java).map { parameters -> + parameters.name to parameters.keyValuePairs.toList() + }.takeIf { it.isNotEmpty() }?.let { rawValues -> + val groupedValues = rawValues.groupBy({ it.first }, { it.second }) + // Verify there are no duplicate iteration names. + groupedValues.forEach { (iterationName, iterations) -> + check(iterations.size == 1) { + "Encountered duplicate iteration name: $iterationName in method ${method.name}" + } + } + val mappedValues = groupedValues.mapValues { (_, iterations) -> iterations.first() } + ParameterizedMethodDeclaration(method, mappedValues) + } + } + } + + /** + * Defines a parameter that may have an injected value that comes from per-test [Iteration] + * definitions. + * + * It's recommended to make such fields 'lateinit var', and they must be public. + * + * The type of the parameter field will define how [Iteration]-defined values are parsed. The + * current list of supported types: + * - [String]s + * - [Boolean]s + * - [Int]s + * - [Long]s + * - [Float]s + * - [Double]s + */ + @Target(AnnotationTarget.FIELD) annotation class Parameter + + /** + * Specifies that a method in a test that uses a [OppiaParameterizedTestRunner] runner should be + * run multiple times for each [Iteration] specified in the [value] iterations list. + * + * See the KDoc for the runner for example code. + */ + @Target(AnnotationTarget.FUNCTION) annotation class RunParameterized(vararg val value: Iteration) + + // TODO(#4120): Migrate to Kotlin @Repeatable once Kotlin 1.6 is used (see: + // https://youtrack.jetbrains.com/issue/KT-12794). + /** + * Defines an iteration to run as part of a [RunParameterized]-annotated test method. + * + * See the KDoc for the runner for example code. + * + * @property name the name of the iteration (this should be short, but meaningful since it's + * appended to the test name) + * @property keyValuePairs an array of strings of the form "key=value" where 'key' is the name of + * a [Parameter]-annotated field and 'value' is a stringified conforming value based on the + * type of that field (incompatible values will result in test failures) + */ + @Repeatable(RunParameterized::class) + @Target(AnnotationTarget.FUNCTION) + annotation class Iteration(val name: String, vararg val keyValuePairs: String) + + private data class ParameterizedMethodDeclaration( + val method: Method, val rawValues: Map> + ) + + private class ProxyParameterizedTestRunner( + private val testClass: Class<*>, + private val parameterizedMethods: Map, + private val methodName: String?, + private val iterationName: String? = null + ): Runner(), Filterable, Sortable { + private val delegate by lazy { constructDelegate() } + private val delegateRunner by lazy { + checkNotNull(delegate as? Runner) { "Delegate runner isn't a JUnit runner: $delegate" } + } + private val delegateFilter by lazy { + checkNotNull(delegate as? Filterable) { "Delegate runner isn't filterable: $delegate" } + } + private val delegateSortable by lazy { + checkNotNull(delegate as? Sortable) { "Delegate runner isn't sortable: $delegate" } + } + + override fun getDescription(): Description = delegateRunner.description + + override fun run(notifier: RunNotifier?) = delegateRunner.run(notifier) + + override fun filter(filter: Filter?) = delegateFilter.filter(filter) + + override fun sort(sorter: Sorter?) = delegateSortable.sort(sorter) + + private fun constructDelegate(): Any { + System.getProperty("android.junit.runner").also { customRunner -> + check(customRunner == null) { + "Detected a custom runner ($customRunner) in a parameterized test. This isn't yet" + + " supported." + } + } + val runningOnAndroid = + System.getProperty("java.runtime.name")?.contains("android", ignoreCase = true) ?: false + + // Load the runner class using reflection since the Robolectric implementation relies on + // Robolectric (which can't be pulled into Espresso builds of shared tests). + val runnerClass = try { + if (runningOnAndroid) { + Class.forName("org.oppia.android.testing.junit.ParameterizedAndroidJUnit4ClassRunner") + } else Class.forName("org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner") + } catch (e: Exception) { + throw IllegalStateException( + "Failed to load delegate test runner class. Did you forget to add either" + + " parameterized_android_junit4_class_runner or parameterized_robolectric_test_runner" + + " as a dependency?", + e + ) + } + + val constructor = + runnerClass.getConstructor( + Class::class.java, Map::class.java, String::class.java, String::class.java + ) + return constructor.newInstance(testClass, parameterizedMethods, methodName, iterationName) + } + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt new file mode 100644 index 00000000000..360a44649e0 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt @@ -0,0 +1,143 @@ +package org.oppia.android.testing.junit + +import java.lang.reflect.Field + +/** + * Represents a single parameterized value for one parameterized field defined by a single test + * method iteration. + * + * @property key the name of the field to which this value is associated + * @property value the type-correct value to assign to the field prior to executing the iteration + * corresponding to this value + */ +internal sealed class ParameterValue(val key: String, val value: Any) { + private class BooleanParameterValue private constructor( + key: String, value: Boolean + ): ParameterValue(key, value) { + companion object { + /** + * Returns a new [ParameterValue] for the specified [key] and a [Boolean] parsed + * representation of [rawValue], or null if the value isn't a valid stringified [Boolean]. + */ + internal fun createParameter(key: String, rawValue: String): ParameterValue? { + return rawValue.toBooleanStrictOrNull()?.let { BooleanParameterValue(key, it) } + } + + // This can be replaced with Kotlin's version once the codebase uses 1.5+. + private fun String.toBooleanStrictOrNull(): Boolean? { + return when (this) { + "true" -> true + "false" -> false + else -> null + } + } + } + } + + private class IntParameterValue private constructor( + key: String, value: Int + ): ParameterValue(key, value) { + companion object { + /** + * Returns a new [ParameterValue] for the specified [key] and an [Int] parsed representation + * of [rawValue], or null if the value isn't a valid stringified [Int]. + */ + internal fun createParameter(key: String, rawValue: String): ParameterValue? { + return rawValue.toIntOrNull()?.let { IntParameterValue(key, it) } + } + } + } + + private class LongParameterValue private constructor( + key: String, value: Long + ): ParameterValue(key, value) { + companion object { + /** + * Returns a new [ParameterValue] for the specified [key] and a [Long] parsed representation + * of [rawValue], or null if the value isn't a valid stringified [Long]. + */ + internal fun createParameter(key: String, rawValue: String): ParameterValue? { + return rawValue.toLongOrNull()?.let { LongParameterValue(key, it) } + } + } + } + + private class FloatParameterValue private constructor( + key: String, value: Float + ): ParameterValue(key, value) { + companion object { + /** + * Returns a new [ParameterValue] for the specified [key] and a [Float] parsed representation + * of [rawValue], or null if the value isn't a valid stringified [Float]. + */ + internal fun createParameter(key: String, rawValue: String): ParameterValue? { + return rawValue.toFloatOrNull()?.let { FloatParameterValue(key, it) } + } + } + } + + private class DoubleParameterValue private constructor( + key: String, value: Double + ): ParameterValue(key, value) { + companion object { + /** + * Returns a new [ParameterValue] for the specified [key] and a [Double] parsed representation + * of [rawValue], or null if the value isn't a valid stringified [Double]. + */ + internal fun createParameter(key: String, rawValue: String): ParameterValue? { + return rawValue.toDoubleOrNull()?.let { DoubleParameterValue(key, it) } + } + } + } + + private class StringParameterValue private constructor( + key: String, value: String + ): ParameterValue(key, value) { + companion object { + /** + * Returns a new [ParameterValue] for the specified [key] and a [String] parsed representation + * of [rawValue], or null if the value isn't a valid stringified [String]. + */ + internal fun createParameter(key: String, rawValue: String): ParameterValue = + StringParameterValue(key, rawValue) + } + } + + internal companion object { + private val booleanValueParser = createParser(BooleanParameterValue::createParameter) + private val intValueParser = createParser(IntParameterValue::createParameter) + private val longValueParser = createParser(LongParameterValue::createParameter) + private val floatValueParser = createParser(FloatParameterValue::createParameter) + private val doubleValueParser = createParser(DoubleParameterValue::createParameter) + private val stringValueParser = createParser(StringParameterValue::createParameter) + + /** + * Returns a new [ParameterValueParser] corresponding to the type of the specified [field], or + * null if the field's type is unsupported. + */ + fun createParserForField(field: Field): ParameterValueParser? { + return when (field.type) { + Boolean::class.java -> booleanValueParser + Int::class.java -> intValueParser + Long::class.java -> longValueParser + Float::class.java -> floatValueParser + Double::class.java -> doubleValueParser + String::class.java -> stringValueParser + else -> null + } + } + + /** A string parser for a specific [ParameterValue] type. */ + fun interface ParameterValueParser { + /** + * Returns a [ParameterValue] corresponding to the specified [key], and with a type-safe + * parsing of [rawValue], or null if the string value is invalid. + */ + fun parseParameter(key: String, rawValue: String): ParameterValue? + } + + // A hack to work around the fact that Kotlin doesn't support assignment conversion from + // references to a functional interface. + private fun createParser(parser: ParameterValueParser) = parser + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt new file mode 100644 index 00000000000..fcb642a437c --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt @@ -0,0 +1,36 @@ +package org.oppia.android.testing.junit + +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import org.junit.runners.model.FrameworkMethod +import org.junit.runners.model.Statement + +/** + * A [AndroidJUnit4ClassRunner] which supports [OppiaParameterizedTestRunner] when running on an + * Espresso-driven platform. + */ +@Suppress("unused") // This class is constructed using reflection. +internal class ParameterizedAndroidJUnit4ClassRunner( + testClass: Class<*>, + private val parameterizedMethods: Map, + private val methodName: String?, + private val iterationName: String? +): AndroidJUnit4ClassRunner(testClass), ParameterizedRunnerOverrideMethods { + private val delegate by lazy { + ParameterizedRunnerDelegate( + parameterizedMethods, + methodName, + iterationName + ).also { delegate -> + delegate.fetchChildrenFromParent = { super.getChildren() } + delegate.fetchTestNameFromParent = { method -> super.testName(method) } + delegate.fetchMethodInvokerFromParent = { method, test -> super.methodInvoker(method, test) } + } + } + + override fun getChildren(): MutableList = delegate.getChildren() + + override fun testName(method: FrameworkMethod?): String = delegate.testName(method) + + override fun methodInvoker(method: FrameworkMethod?, test: Any?): Statement = + delegate.methodInvoker(method, test) +} diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedMethod.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedMethod.kt new file mode 100644 index 00000000000..d56cb4e3e87 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedMethod.kt @@ -0,0 +1,44 @@ +package org.oppia.android.testing.junit + +import java.lang.reflect.Field +import java.util.Locale + +/** + * A parameterized method used by [OppiaParameterizedTestRunner] when defining sub-tests that are + * run as part of the test suite. + * + * @property methodName the name of the test method that's been parameterized + */ +internal class ParameterizedMethod( + val methodName: String, + private val values: Map>, + private val parameterFields: List +) { + /** The names of all iterations run for this method. */ + val iterationNames: Collection by lazy { values.keys } + + /** + * Updates the specified [testClassInstance]'s parameter-injected fields to the values + * corresponding to the specified [iterationName] iteration. + * + * This should always be called before the test's execution begins. It's also expected that this + * method is called for each iteration (since the test method should be executed multiples, once + * for each of its iteration). + */ + internal fun initializeForTest(testClassInstance: Any, iterationName: String) { + // Retrieve the setters for the fields (since these are expected to be used instead of direct + // property access in Kotlin). Note that these need to be re-fetched since the instance class + // may change (due to Robolectric instrumentation including custom class loading & bytecode + // changes). + val baseClass = testClassInstance.javaClass + val fieldSetters = parameterFields.map { field -> + val setterMethod = + baseClass.getDeclaredMethod("set${field.name.capitalize(Locale.US)}", field.type) + field.name to setterMethod + }.toMap() + values.getValue(iterationName).forEach { parameterValue -> + val fieldSetter = fieldSetters.getValue(parameterValue.key) + fieldSetter.invoke(testClassInstance, parameterValue.value) + } + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt new file mode 100644 index 00000000000..1a6df490125 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt @@ -0,0 +1,49 @@ +package org.oppia.android.testing.junit + +import org.junit.runners.model.FrameworkMethod +import org.junit.runners.model.Statement +import org.robolectric.RobolectricTestRunner + +/** + * A [RobolectricTestRunner] which supports [OppiaParameterizedTestRunner] when running on a local + * JVM using Robolectric. + */ +@Suppress("unused") // This class is constructed using reflection. +internal class ParameterizedRobolectricTestRunner( + testClass: Class<*>, + private val parameterizedMethods: Map, + private val methodName: String?, + private val iterationName: String? +): RobolectricTestRunner(testClass), ParameterizedRunnerOverrideMethods { + private val delegate by lazy { + ParameterizedRunnerDelegate( + parameterizedMethods, + methodName, + iterationName + ).also { delegate -> + delegate.fetchChildrenFromParent = { super.getChildren() } + delegate.fetchTestNameFromParent = { method -> super.testName(method) } + } + } + + override fun getChildren(): MutableList = delegate.getChildren() + + override fun testName(method: FrameworkMethod?): String = delegate.testName(method) + + override fun methodInvoker(method: FrameworkMethod?, test: Any?): Nothing { + throw Exception("Expected this to never be executed in the Robolectric environment.") + } + + override fun getHelperTestRunner( + bootstrappedTestClass: Class<*>? + ): HelperTestRunner { + return object: HelperTestRunner(bootstrappedTestClass) { + override fun methodInvoker(method: FrameworkMethod?, test: Any?): Statement { + delegate.fetchMethodInvokerFromParent = { innerMethod, innerParent -> + super.methodInvoker(innerMethod, innerParent) + } + return delegate.methodInvoker(method, test) + } + } + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt new file mode 100644 index 00000000000..02d90dbd5d8 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt @@ -0,0 +1,67 @@ +package org.oppia.android.testing.junit + +import org.junit.runners.model.FrameworkMethod +import org.junit.runners.model.Statement + +/** + * A common helper for platform-specific helper runners. + * + * This class performs the actual field injection and execution delegation for running each + * parameterized test method. + */ +internal class ParameterizedRunnerDelegate( + private val parameterizedMethods: Map, + private val methodName: String?, + private val iterationName: String? +): ParameterizedRunnerOverrideMethods { + /** + * A lambda used to call into the parent runner's [getChildren] method. This should be set by + * helper parameterized test runners. + */ + lateinit var fetchChildrenFromParent: () -> MutableList + + /** + * A lambda used to call into the parent runner's [testName] method. This should be set by helper + * parameterized test runners. + */ + lateinit var fetchTestNameFromParent: (FrameworkMethod?) -> String + + /** + * A lambda used to call into the parent runner's [methodInvoker] method. This should be set by + * helper parameterized test runners. + */ + lateinit var fetchMethodInvokerFromParent: (FrameworkMethod?, Any?) -> Statement + + override fun getChildren(): MutableList { + return fetchChildrenFromParent().filter { + // Either only match to the specific method, or no parameterized methods. + if (methodName != null) { + it.method.name == methodName + } else it.method.name !in parameterizedMethods.keys + }.toMutableList() + } + + override fun testName(method: FrameworkMethod?): String { + return if (methodName != null) { + "${fetchTestNameFromParent(method)}-$iterationName" + } else fetchTestNameFromParent(method) + } + + override fun methodInvoker(method: FrameworkMethod?, test: Any?): Statement { + val invoker = fetchMethodInvokerFromParent(method, test) + checkNotNull(test) { "Expected test to be initialized." } + return if (methodName != null && iterationName != null) { + val parameterizedMethod = checkNotNull(parameterizedMethods[method?.name]) { + "Expected to find registered parameterized method: ${method?.name}, available:" + + " ${parameterizedMethods.keys}" + } + object : Statement() { + override fun evaluate() { + // Initialize the test prior to execution. + parameterizedMethod.initializeForTest(test, iterationName) + invoker.evaluate() + } + } + } else invoker + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerOverrideMethods.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerOverrideMethods.kt new file mode 100644 index 00000000000..6d591ec274f --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerOverrideMethods.kt @@ -0,0 +1,18 @@ +package org.oppia.android.testing.junit + +import org.junit.runners.model.FrameworkMethod +import org.junit.runners.model.Statement + +/** + * Specifies methods that the helper parameterized runners should override from JUnit's test runner. + */ +internal interface ParameterizedRunnerOverrideMethods { + /** See [org.junit.runners.BlockJUnit4ClassRunner.getChildren]. */ + fun getChildren(): MutableList + + /** See [org.junit.runners.BlockJUnit4ClassRunner.testName]. */ + fun testName(method: FrameworkMethod?): String + + /** See [org.junit.runners.BlockJUnit4ClassRunner.methodInvoker]. */ + fun methodInvoker(method: FrameworkMethod?, test: Any?): Statement +} diff --git a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel index 7599a2c3008..ea0b8ba6b54 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel @@ -108,3 +108,18 @@ kt_android_library( "//third_party:com_google_truth_truth", ], ) + +kt_android_library( + name = "token_subject", + testonly = True, + srcs = [ + "TokenSubject.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + "//third_party:com_google_truth_truth", + "//utility/src/main/java/org/oppia/android/util/math:tokenizer", + ], +) diff --git a/testing/src/main/java/org/oppia/android/testing/math/TokenSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/TokenSubject.kt new file mode 100644 index 00000000000..a623c59c7b6 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/TokenSubject.kt @@ -0,0 +1,173 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.BooleanSubject +import com.google.common.truth.DoubleSubject +import com.google.common.truth.FailureMetadata +import com.google.common.truth.IntegerSubject +import com.google.common.truth.StringSubject +import com.google.common.truth.Subject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import org.oppia.android.testing.math.TokenSubject.Companion.assertThat +import org.oppia.android.util.math.MathTokenizer.Companion.Token +import org.oppia.android.util.math.MathTokenizer.Companion.Token.DivideSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.EqualsSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.ExponentiationSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.FunctionName +import org.oppia.android.util.math.MathTokenizer.Companion.Token.IncompleteFunctionName +import org.oppia.android.util.math.MathTokenizer.Companion.Token.InvalidToken +import org.oppia.android.util.math.MathTokenizer.Companion.Token.LeftParenthesisSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.MinusSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.MultiplySymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.PlusSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.PositiveInteger +import org.oppia.android.util.math.MathTokenizer.Companion.Token.PositiveRealNumber +import org.oppia.android.util.math.MathTokenizer.Companion.Token.RightParenthesisSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.SquareRootSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.VariableName + +// TODO(#4121): Add tests for this class. + +/** + * Truth subject for verifying properties of [Token]s. + * + * Call [assertThat] to create the subject. + */ +class TokenSubject( + metadata: FailureMetadata, + private val actual: Token +) : Subject(metadata, actual) { + /** Returns an [IntegerSubject] to test [Token.startIndex]. */ + fun hasStartIndexThat(): IntegerSubject = assertThat(actual.startIndex) + + /** Returns an [IntegerSubject] to test [Token.endIndex]. */ + fun hasEndIndexThat(): IntegerSubject = assertThat(actual.endIndex) + + /** + * Verifies that the [Token] being tested is a [PositiveInteger], and returns an [IntegerSubject] + * to test its [PositiveInteger.parsedValue]. + */ + fun isPositiveIntegerWhoseValue(): IntegerSubject { + return assertThat(actual.asVerifiedType().parsedValue) + } + + /** + * Verifies that the [Token] being tested is a [PositiveRealNumber], and returns a [DoubleSubject] + * to test its [PositiveRealNumber.parsedValue]. + */ + fun isPositiveRealNumberWhoseValue(): DoubleSubject { + return assertThat(actual.asVerifiedType().parsedValue) + } + + /** + * Verifies that the [Token] being tested is a [VariableName], and returns a [StringSubject] to + * test its [VariableName.parsedName]. + */ + fun isVariableWhoseName(): StringSubject { + return assertThat(actual.asVerifiedType().parsedName) + } + + /** + * Verifies that the [Token] being tested is a [FunctionName], and returns a [FunctionNameSubject] + * to test specific attributes of the function name. + */ + fun isFunctionNameThat(): FunctionNameSubject { + return FunctionNameSubject.assertThat(actual.asVerifiedType()) + } + + /** Verifies that the [Token] being tested is a [MinusSymbol]. */ + fun isMinusSymbol() { + actual.asVerifiedType() + } + + /** Verifies that the [Token] being tested is a [SquareRootSymbol]. */ + fun isSquareRootSymbol() { + actual.asVerifiedType() + } + + /** Verifies that the [Token] being tested is a [PlusSymbol]. */ + fun isPlusSymbol() { + actual.asVerifiedType() + } + + /** Verifies that the [Token] being tested is a [MultiplySymbol]. */ + fun isMultiplySymbol() { + actual.asVerifiedType() + } + + /** Verifies that the [Token] being tested is a [DivideSymbol]. */ + fun isDivideSymbol() { + actual.asVerifiedType() + } + + /** Verifies that the [Token] being tested is an [ExponentiationSymbol]. */ + fun isExponentiationSymbol() { + actual.asVerifiedType() + } + + /** Verifies that the [Token] being tested is an [EqualsSymbol]. */ + fun isEqualsSymbol() { + actual.asVerifiedType() + } + + /** Verifies that the [Token] being tested is a [LeftParenthesisSymbol]. */ + fun isLeftParenthesisSymbol() { + actual.asVerifiedType() + } + + /** Verifies that the [Token] being tested is a [RightParenthesisSymbol]. */ + fun isRightParenthesisSymbol() { + actual.asVerifiedType() + } + + /** Verifies that the [Token] being tested is an [InvalidToken]. */ + fun isInvalidToken() { + actual.asVerifiedType() + } + + /** Verifies that the [Token] being tested is an [IncompleteFunctionName]. */ + fun isIncompleteFunctionName() { + actual.asVerifiedType() + } + + /** + * Truth subject for verifying properties of [Token]FunctionName. + * + * Call [assertThat] to create the subject. + */ + class FunctionNameSubject( + metadata: FailureMetadata, + private val actual: FunctionName + ) : Subject(metadata, actual) { + /** + * Returns a [StringSubject] to test the value of [FunctionName.parsedName] for the function + * name being tested by this subject. + */ + fun hasNameThat(): StringSubject = assertThat(actual.parsedName) + + /** + * Returns a [BooleanSubject] to test the value of [FunctionName.isAllowedFunction] for the + * function name being tested by this subject. + */ + fun hasIsAllowedPropertyThat(): BooleanSubject = assertThat(actual.isAllowedFunction) + + companion object { + /** + * Returns a new [FunctionNameSubject] to verify aspects of the specified [FunctionName] + * value. + */ + internal fun assertThat(actual: FunctionName): FunctionNameSubject = + assertAbout(::FunctionNameSubject).that(actual) + } + } + + companion object { + /** Returns a new [TokenSubject] to verify aspects of the specified [Token] value. */ + fun assertThat(actual: Token): TokenSubject = assertAbout(::TokenSubject).that(actual) + + private inline fun Token.asVerifiedType(): T { + assertThat(this).isInstanceOf(T::class.java) + return this as T + } + } +} From 83e800cdb2252fd167f3597e076c5854569d59fe Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 20 Jan 2022 22:24:11 -0800 Subject: [PATCH 047/134] Add & update tests. This introduces tests for PeekableIterator, and reimplements all of MathTokenizer's tests to be more structured, thorough, and a bit more maintainable (i.e. by leveraging parameterized tests). --- .../org/oppia/android/util/math/BUILD.bazel | 3 + .../oppia/android/util/math/MathTokenizer.kt | 94 +- .../android/util/math/PeekableIterator.kt | 66 +- .../org/oppia/android/util/math/BUILD.bazel | 22 +- .../android/util/math/MathTokenizerTest.kt | 849 ++++++++++++++---- .../android/util/math/PeekableIteratorTest.kt | 600 +++++++++++++ 6 files changed, 1446 insertions(+), 188 deletions(-) create mode 100644 utility/src/test/java/org/oppia/android/util/math/PeekableIteratorTest.kt 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 index 3d3925115ee..9250d3ffba1 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -40,4 +40,7 @@ kt_android_library( srcs = [ "PeekableIterator.kt", ], + visibility = [ + "//:oppia_testing_visibility", + ], ) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt index 37ca0410cd0..b710ae7908b 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt @@ -10,19 +10,32 @@ import org.oppia.android.app.model.MathUnaryOperation import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE import java.lang.StringBuilder - -// TODO: rename to MathTokenizer & add documentation. -// TODO: consider a more efficient implementation that uses 1 underlying buffer (which could still -// be sequence-backed) with a forced lookahead-of-1 API, to also avoid rebuffering when parsing -// sequences of characters like for integers. - -// TODO: add customization to omit certain symbols (such as variables for numeric expressions?) +import org.oppia.android.util.math.PeekableIterator.Companion.toPeekableIterator + +/** + * Input tokenizer for math (numeric & algebraic) expressions and equations. + * + * See https://docs.google.com/document/d/1JMpbjqRqdEpye67HvDoqBo_rtScY9oEaB7SwKBBspss/edit for the + * grammar specification supported by this tokenizer. + * + * This class implements an LL(1) single-pass tokenizer with no caching. Use [tokenize] to produce a + * sequence of [Token]s from the given input stream. + */ class MathTokenizer private constructor() { companion object { + /** + * Returns a [Sequence] of [Token]s for the specified input string. + * + * Note that this tokenizer will attempt to recover if an invalid token is encountered (i.e. + * tokenization will continue). Further, tokenization occurs lazily (i.e. as the sequence is + * traversed), so calling this method is essentially zero-cost until tokens are actually needed. + * The sequence should be converted to a [List] if they need to be retained after initial + * tokenization since the sequence retains no memory. + */ fun tokenize(input: String): Sequence = tokenize(input.toCharArray().asSequence()) - fun tokenize(input: Sequence): Sequence { - val chars = PeekableIterator.fromSequence(input) + private fun tokenize(input: Sequence): Sequence { + val chars = input.toPeekableIterator() return generateSequence { // Consume any whitespace that might precede a valid token. chars.consumeWhitespace() @@ -37,7 +50,6 @@ class MathTokenizer private constructor() { '+' -> tokenizeSymbol(chars) { startIndex, endIndex -> Token.PlusSymbol(startIndex, endIndex) } - // TODO: add tests for different subtraction/minus symbols. '-', '−' -> tokenizeSymbol(chars) { startIndex, endIndex -> Token.MinusSymbol(startIndex, endIndex) } @@ -73,6 +85,7 @@ class MathTokenizer private constructor() { val integerPart1 = parseInteger(chars) ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) + val integerEndIndex = chars.getRetrievalCount() // The end index for integers. chars.consumeWhitespace() // Whitespace is allowed between digits and the '.'. return if (chars.peek() == '.') { chars.next() // Parse the "." since it will be re-added later. @@ -82,8 +95,7 @@ class MathTokenizer private constructor() { val integerPart2 = parseInteger(chars) ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) - // TODO: validate that the result isn't NaN or INF. - val doubleValue = "$integerPart1.$integerPart2".toDoubleOrNull() + val doubleValue = "$integerPart1.$integerPart2".toValidDoubleOrNull() ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) Token.PositiveRealNumber(doubleValue, startIndex, endIndex = chars.getRetrievalCount()) } else { @@ -91,7 +103,7 @@ class MathTokenizer private constructor() { integerPart1.toIntOrNull() ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()), startIndex, - endIndex = chars.getRetrievalCount() + integerEndIndex ) } } @@ -250,39 +262,77 @@ class MathTokenizer private constructor() { } else null // Failed to parse; no digits. } + private fun String.toValidDoubleOrNull(): Double? { + return toDoubleOrNull()?.takeIf { it.isFinite() } + } + + /** Represents a token that may act as a unary operator. */ interface UnaryOperatorToken { + /** + * Returns the [MathUnaryOperation.Operator] that would be associated with this token if it's + * treated as a unary operator. + */ fun getUnaryOperator(): MathUnaryOperation.Operator } + /** Represents a token that may act as a binary operator. */ interface BinaryOperatorToken { + /** + * Returns the [MathBinaryOperation.Operator] that would be associated with this token if it's + * treated as a binary operator. + */ fun getBinaryOperator(): MathBinaryOperation.Operator } + /** Represents a token that may be encountered during tokenization. */ sealed class Token { - /** The index in the input stream at which point this token begins. */ + /** The (inclusive) index in the input stream at which point this token begins. */ abstract val startIndex: Int /** The (exclusive) index in the input stream at which point this token ends. */ abstract val endIndex: Int + /** + * Represents a positive integer (i.e. no decimal point, and no negative sign). + * + * @property parsedValue the parsed value of the integer + */ class PositiveInteger( val parsedValue: Int, override val startIndex: Int, override val endIndex: Int ) : Token() + /** + * Represents a positive real number (i.e. contains a decimal point, but no negative sign). + * + * @property parsedValue the parsed value of the real number as a [Double] + */ class PositiveRealNumber( val parsedValue: Double, override val startIndex: Int, override val endIndex: Int ) : Token() + /** + * Represents a variable. + * + * @property parsedName the name of the variable + */ class VariableName( val parsedName: String, override val startIndex: Int, override val endIndex: Int ) : Token() + /** + * Represents a recognized function name (otherwise sequential letters are treated as + * variables), e.g.: sqrt. + * + * @property parsedName the name of the function + * @property isAllowedFunction whether the function is supported by the parser. This helps + * with error detection & management while parsing. + */ class FunctionName( val parsedName: String, val isAllowedFunction: Boolean, @@ -290,6 +340,10 @@ class MathTokenizer private constructor() { override val endIndex: Int ) : Token() + /** Represents a square root sign, i.e. '√'. */ + class SquareRootSymbol(override val startIndex: Int, override val endIndex: Int) : Token() + + /** Represents a minus sign, e.g. '-'. */ class MinusSymbol( override val startIndex: Int, override val endIndex: Int @@ -299,8 +353,7 @@ class MathTokenizer private constructor() { override fun getBinaryOperator(): MathBinaryOperation.Operator = SUBTRACT } - class SquareRootSymbol(override val startIndex: Int, override val endIndex: Int) : Token() - + /** Represents a plus sign, e.g. '+'. */ class PlusSymbol( override val startIndex: Int, override val endIndex: Int @@ -310,6 +363,7 @@ class MathTokenizer private constructor() { override fun getBinaryOperator(): MathBinaryOperation.Operator = ADD } + /** Represents a multiply sign, e.g. '*'. */ class MultiplySymbol( override val startIndex: Int, override val endIndex: Int @@ -317,6 +371,7 @@ class MathTokenizer private constructor() { override fun getBinaryOperator(): MathBinaryOperation.Operator = MULTIPLY } + /** Represents a divide sign, e.g. '/'. */ class DivideSymbol( override val startIndex: Int, override val endIndex: Int @@ -324,6 +379,7 @@ class MathTokenizer private constructor() { override fun getBinaryOperator(): MathBinaryOperation.Operator = DIVIDE } + /** Represents an exponent sign, i.e. '^'. */ class ExponentiationSymbol( override val startIndex: Int, override val endIndex: Int @@ -331,27 +387,31 @@ class MathTokenizer private constructor() { override fun getBinaryOperator(): MathBinaryOperation.Operator = EXPONENTIATE } + /** Represents an equals sign, i.e. '='. */ class EqualsSymbol(override val startIndex: Int, override val endIndex: Int) : Token() + /** Represents a left parenthesis symbol, i.e. '('. */ class LeftParenthesisSymbol( override val startIndex: Int, override val endIndex: Int ) : Token() + /** Represents a right parenthesis symbol, i.e. ')'. */ class RightParenthesisSymbol( override val startIndex: Int, override val endIndex: Int ) : Token() + /** Represents an incomplete function name, e.g. 'sqr'. */ class IncompleteFunctionName( override val startIndex: Int, override val endIndex: Int ) : Token() + /** Represents an invalid character that doesn't fit any of the other [Token] types. */ class InvalidToken(override val startIndex: Int, override val endIndex: Int) : Token() } - // TODO: consider whether to use the more correct & expensive Java Char.isWhitespace(). private fun Char.isWhitespace(): Boolean = when (this) { ' ', '\t', '\n', '\r' -> true else -> false diff --git a/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt b/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt index 1a7abacc061..4d752fdc2f1 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt @@ -1,16 +1,36 @@ package org.oppia.android.util.math -class PeekableIterator(private val backingIterator: Iterator) : Iterator { +/** + * An [Iterator] for [Sequence]s that may have exactly up to 1 element lookahead. + * + * This iterator is intended to be used by tokenizers and parsers to force an LL(1) implementation + * of both (since only one token may be observed before retrieval). Further, this implementation is + * intentionally limited to sequences for potential performance: since no more than one element + * requires lookahead, having a backed array is unnecessary which means a potentially expensive and + * fully dynamic sequence could back the iterator. While especially large inputs aren't expected, + * they are inherently supported by the implementation with no additional overhead. + * + * New implementations can be created using [toPeekableIterator]. + * + * This class is not safe to use across multiple threads and requires synchronization. + */ +class PeekableIterator private constructor( + private val backingIterator: Iterator +) : Iterator { private var next: T? = null private var count: Int = 0 override fun hasNext(): Boolean = next != null || backingIterator.hasNext() - override fun next(): T = next?.also { - next = null - count++ - } ?: retrieveNext() + override fun next(): T = (next?.also { next = null } ?: retrieveNext()).also { count++ } + /** + * Returns the next element to be returned by [next] without actually consuming it, or ``null`` if + * there isn't one (i.e. [hasNext] returns false). + * + * It's safe to call this both at the end of the iterator, and multiple times (at any point in the + * iteration). + */ fun peek(): T? { return when { next != null -> next @@ -19,20 +39,52 @@ class PeekableIterator(private val backingIterator: Iterator) : Iter } } + /** + * Consumes and returns the next token if it matches the value provided by [expected]. + * + * This method essentially behaves the same way as [expectNextMatches]. + */ fun expectNextValue(expected: () -> T): T? = expectNextMatches { it == expected() } + /** + * Consumes and returns the next token (if it's available--see [peek]) if it passes the specified + * [predicate], otherwise ``null`` is returned. + * + * Note that a ``nul`` return value isn't sufficient to determine the iterator has ended + * ([hasNext] or [peek] should be used for that, instead). + * + * Note also that [predicate] will only be called once, but no assumption should be made as to + * when it will be called. + */ fun expectNextMatches(predicate: (T) -> Boolean): T? { // Only call the predicate if not at the end of the stream, and only call next() if the next // value matches. return peek()?.takeIf(predicate)?.also { next() } } + /** + * Returns the number of elements consumed by this iterator so far via [next]. + * + * At the beginning of iteration, this will return 0. At the end (i.e. when [hasNext] returns + * false), this will return the size of the underlying sequence/container (depending on if + * iteration began at the beginning of the sequence--see the caveat in [toPeekableIterator] for + * specifics). + */ fun getRetrievalCount(): Int = count private fun retrieveNext(): T = backingIterator.next() companion object { - fun fromSequence(sequence: Sequence): PeekableIterator = - PeekableIterator(sequence.iterator()) + /** + * Returns a new [PeekableIterator] for this [Sequence]. + * + * Note that iteration begins at the current point of the [Sequence] (since sequences may not + * retain state and can't be rewinded), so care should be taken when using multiple iterators + * for the same sequence (including when converting the sequence to another structure, like a + * [List]). Some sequences do support multiple iteration, so the exact behavior of the returned + * iterator will be sequence implementation dependent. + */ + fun Sequence.toPeekableIterator(): PeekableIterator = + PeekableIterator(iterator()) } } 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 index cd67c41492f..27c60ff1941 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -49,7 +49,9 @@ oppia_android_test( deps = [ "//model/src/main/proto:math_java_proto_lite", "//testing:assertion_helpers", - "//third_party:androidx_test_ext_junit", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/math:token_subject", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", @@ -58,6 +60,24 @@ oppia_android_test( ], ) +oppia_android_test( + name = "PeekableIteratorTest", + srcs = ["PeekableIteratorTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.PeekableIteratorTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//testing", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_mockito_mockito-core", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:peekable_iterator", + ], +) + oppia_android_test( name = "PolynomialExtensionsTest", srcs = ["PolynomialExtensionsTest.kt"], diff --git a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt index ac7e6556b9c..276911cb62a 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt @@ -1,195 +1,718 @@ package org.oppia.android.util.math -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.common.truth.DoubleSubject -import com.google.common.truth.FailureMetadata -import com.google.common.truth.IntegerSubject -import com.google.common.truth.StringSubject -import com.google.common.truth.Subject -import com.google.common.truth.Truth.assertAbout import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.util.math.MathTokenizer.Companion.Token +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.math.TokenSubject.Companion.assertThat import org.robolectric.annotation.LooperMode /** Tests for [MathTokenizer]. */ -@RunWith(AndroidJUnit4::class) +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(OppiaParameterizedTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class MathTokenizerTest { + @Parameter lateinit var variableName: String + @Parameter lateinit var funcName: String + @Parameter lateinit var token: String + @Test - fun testLotsOfCases() { - // TODO: split this up - // testTokenize_emptyString_producesNoTokens - val tokens1 = MathTokenizer.tokenize(" ").toList() - assertThat(tokens1).isEmpty() - - val tokens2 = MathTokenizer.tokenize(" 2 ").toList() - assertThat(tokens2).hasSize(1) - assertThat(tokens2.first()).isPositiveIntegerWhoseValue().isEqualTo(2) - - val tokens3 = MathTokenizer.tokenize(" 2.5 ").toList() - assertThat(tokens3).hasSize(1) - assertThat(tokens3.first()).isPositiveRealNumberWhoseValue().isWithin(1e-5).of(2.5) - - val tokens4 = MathTokenizer.tokenize(" x ").toList() - assertThat(tokens4).hasSize(1) - assertThat(tokens4.first()).isVariableWhoseName().isEqualTo("x") - - val tokens5 = MathTokenizer.tokenize(" z x ").toList() - assertThat(tokens5).hasSize(2) - assertThat(tokens5[0]).isVariableWhoseName().isEqualTo("z") - assertThat(tokens5[1]).isVariableWhoseName().isEqualTo("x") - - val tokens6 = MathTokenizer.tokenize("2^3^2").toList() - assertThat(tokens6).hasSize(5) - assertThat(tokens6[0]).isPositiveIntegerWhoseValue().isEqualTo(2) - assertThat(tokens6[1]).isExponentiationSymbol() - assertThat(tokens6[2]).isPositiveIntegerWhoseValue().isEqualTo(3) - assertThat(tokens6[3]).isExponentiationSymbol() - assertThat(tokens6[4]).isPositiveIntegerWhoseValue().isEqualTo(2) - - val tokens7 = MathTokenizer.tokenize("sqrt(2)").toList() - assertThat(tokens7).hasSize(4) - assertThat(tokens7[0]).isFunctionWhoseName().isEqualTo("sqrt") - assertThat(tokens7[1]).isLeftParenthesisSymbol() - assertThat(tokens7[2]).isPositiveIntegerWhoseValue().isEqualTo(2) - assertThat(tokens7[3]).isRightParenthesisSymbol() - - val tokens8 = MathTokenizer.tokenize("sqr(2)").toList() - assertThat(tokens8).hasSize(4) - assertThat(tokens8[0]).isIncompleteFunctionName() - assertThat(tokens8[1]).isLeftParenthesisSymbol() - assertThat(tokens8[2]).isPositiveIntegerWhoseValue().isEqualTo(2) - assertThat(tokens8[3]).isRightParenthesisSymbol() - - val tokens9 = MathTokenizer.tokenize("xyz(2)").toList() - assertThat(tokens9).hasSize(6) - assertThat(tokens9[0]).isVariableWhoseName().isEqualTo("x") - assertThat(tokens9[1]).isVariableWhoseName().isEqualTo("y") - assertThat(tokens9[2]).isVariableWhoseName().isEqualTo("z") - assertThat(tokens9[3]).isLeftParenthesisSymbol() - assertThat(tokens9[4]).isPositiveIntegerWhoseValue().isEqualTo(2) - assertThat(tokens9[5]).isRightParenthesisSymbol() - - val tokens10 = MathTokenizer.tokenize("732").toList() - assertThat(tokens10).hasSize(1) - assertThat(tokens10.first()).isPositiveIntegerWhoseValue().isEqualTo(732) - - val tokens11 = MathTokenizer.tokenize("73 2").toList() - assertThat(tokens11).hasSize(2) - assertThat(tokens11[0]).isPositiveIntegerWhoseValue().isEqualTo(73) - assertThat(tokens11[1]).isPositiveIntegerWhoseValue().isEqualTo(2) - - val tokens12 = MathTokenizer.tokenize("1*2-3+4^7-8/3*2+7").toList() - assertThat(tokens12).hasSize(17) - assertThat(tokens12[0]).isPositiveIntegerWhoseValue().isEqualTo(1) - assertThat(tokens12[1]).isMultiplySymbol() - assertThat(tokens12[2]).isPositiveIntegerWhoseValue().isEqualTo(2) - assertThat(tokens12[3]).isMinusSymbol() - assertThat(tokens12[4]).isPositiveIntegerWhoseValue().isEqualTo(3) - assertThat(tokens12[5]).isPlusSymbol() - assertThat(tokens12[6]).isPositiveIntegerWhoseValue().isEqualTo(4) - assertThat(tokens12[7]).isExponentiationSymbol() - assertThat(tokens12[8]).isPositiveIntegerWhoseValue().isEqualTo(7) - assertThat(tokens12[9]).isMinusSymbol() - assertThat(tokens12[10]).isPositiveIntegerWhoseValue().isEqualTo(8) - assertThat(tokens12[11]).isDivideSymbol() - assertThat(tokens12[12]).isPositiveIntegerWhoseValue().isEqualTo(3) - assertThat(tokens12[13]).isMultiplySymbol() - assertThat(tokens12[14]).isPositiveIntegerWhoseValue().isEqualTo(2) - assertThat(tokens12[15]).isPlusSymbol() - assertThat(tokens12[16]).isPositiveIntegerWhoseValue().isEqualTo(7) - - val tokens13 = MathTokenizer.tokenize("x = √2 × 7 ÷ 4").toList() - assertThat(tokens13).hasSize(8) - assertThat(tokens13[0]).isVariableWhoseName().isEqualTo("x") - assertThat(tokens13[1]).isEqualsSymbol() - assertThat(tokens13[2]).isSquareRootSymbol() - assertThat(tokens13[3]).isPositiveIntegerWhoseValue().isEqualTo(2) - assertThat(tokens13[4]).isMultiplySymbol() - assertThat(tokens13[5]).isPositiveIntegerWhoseValue().isEqualTo(7) - assertThat(tokens13[6]).isDivideSymbol() - assertThat(tokens13[7]).isPositiveIntegerWhoseValue().isEqualTo(4) - } - - private class TokenSubject( - metadata: FailureMetadata, - private val actual: T - ) : Subject(metadata, actual) { - fun isPositiveIntegerWhoseValue(): IntegerSubject { - return assertThat(actual.asVerifiedType().parsedValue) - } + fun testTokenize_emptyString_producesNoTokens() { + val tokens = MathTokenizer.tokenize("").toList() - fun isPositiveRealNumberWhoseValue(): DoubleSubject { - return assertThat(actual.asVerifiedType().parsedValue) - } + assertThat(tokens).isEmpty() + } - fun isVariableWhoseName(): StringSubject { - return assertThat(actual.asVerifiedType().parsedName) - } + @Test + fun testTokenize_onlyWhitespace_producesNoTokens() { + val tokens = MathTokenizer.tokenize(" ").toList() - fun isFunctionWhoseName(): StringSubject { - return assertThat(actual.asVerifiedType().parsedName) - } + assertThat(tokens).isEmpty() + } - fun isMinusSymbol() { - actual.asVerifiedType() - } + @Test + fun testTokenize_singleDigit_producesPositiveIntegerToken() { + val tokens = MathTokenizer.tokenize("1").toList() - fun isSquareRootSymbol() { - actual.asVerifiedType() - } + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isPositiveIntegerWhoseValue().isEqualTo(1) + } - fun isPlusSymbol() { - actual.asVerifiedType() - } + @Test + fun testTokenize_digits_producesPositiveIntegerToken() { + val tokens = MathTokenizer.tokenize("927").toList() - fun isMultiplySymbol() { - actual.asVerifiedType() - } + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isPositiveIntegerWhoseValue().isEqualTo(927) + } - fun isDivideSymbol() { - actual.asVerifiedType() - } + @Test + fun testTokenize_digits_withSpaces_spacesAreIgnored() { + val tokens = MathTokenizer.tokenize(" 927 ").toList() - fun isExponentiationSymbol() { - actual.asVerifiedType() - } + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isPositiveIntegerWhoseValue().isEqualTo(927) + } - fun isEqualsSymbol() { - actual.asVerifiedType() - } + @Test + fun testTokenize_positiveInteger_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" 927 ").toList() - fun isLeftParenthesisSymbol() { - actual.asVerifiedType() - } + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(5) + } - fun isRightParenthesisSymbol() { - actual.asVerifiedType() - } + @Test + fun testTokenize_positiveInteger_veryLargeNumber_producesInvalidToken() { + val tokens = MathTokenizer.tokenize("9823190830924801923845").toList() - fun isInvalidToken() { - actual.asVerifiedType() - } + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isInvalidToken() + } - fun isIncompleteFunctionName() { - actual.asVerifiedType() - } + @Test + fun testTokenize_decimal_producesInvalidToken() { + val tokens = MathTokenizer.tokenize(".").toList() + + // A decimal by itself is invalid. + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isInvalidToken() + } + + @Test + fun testTokenize_digitsWithDecimal_producesInvalidToken() { + val tokens = MathTokenizer.tokenize("12.").toList() + + // The decimal is incomplete. Note that this is one token since the '12.' is considered a single + // invalid unit. + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isInvalidToken() + } + + @Test + fun testTokenize_decimalWithDigits_producesInvalidToken() { + val tokens = MathTokenizer.tokenize(".34").toList() + + // The decimal is incomplete. Note that this results in 2 tokens since the '.' is encountered as + // an isolated and unexpected symbol. + assertThat(tokens).hasSize(2) + assertThat(tokens[0]).isInvalidToken() + } + + @Test + fun testTokenize_digitsWithDecimalWithDigits_producesPositiveRealNumberToken() { + val tokens = MathTokenizer.tokenize("12.34").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isPositiveRealNumberWhoseValue().isWithin(1e-5).of(12.34) + } + + @Test + fun testTokenize_positiveRealNumber_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" 12.34 ").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(7) + } + + @Test + fun testTokenize_positiveRealNumber_veryLargeNumber_producesInvalidToken() { + val tokens = MathTokenizer.tokenize("1${"0".repeat(400)}.12345").toList() + + // The number is too large to represent as a double (so it's treated as infinity). + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isInvalidToken() + } + + @Test + fun testTokenize_variable_singleLetter_producesVariableToken() { + val tokens = MathTokenizer.tokenize("x").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isVariableWhoseName().isEqualTo("x") + } + + @Test + fun testTokenize_variable_twoLetters_producesMultipleVariableTokens() { + val tokens = MathTokenizer.tokenize("xy").toList() + + assertThat(tokens).hasSize(2) + assertThat(tokens[0]).isVariableWhoseName().isEqualTo("x") + assertThat(tokens[1]).isVariableWhoseName().isEqualTo("y") + } + + @Test + fun testTokenize_variable_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" x ").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(3) + } + + @Test + @RunParameterized( + Iteration("a", "variableName=a"), Iteration("A", "variableName=A"), + Iteration("b", "variableName=b"), Iteration("B", "variableName=B"), + Iteration("c", "variableName=c"), Iteration("C", "variableName=C"), + Iteration("d", "variableName=d"), Iteration("D", "variableName=D"), + Iteration("e", "variableName=e"), Iteration("E", "variableName=E"), + Iteration("f", "variableName=f"), Iteration("F", "variableName=F"), + Iteration("g", "variableName=g"), Iteration("G", "variableName=G"), + Iteration("h", "variableName=h"), Iteration("H", "variableName=H"), + Iteration("i", "variableName=i"), Iteration("I", "variableName=I"), + Iteration("j", "variableName=j"), Iteration("J", "variableName=J"), + Iteration("k", "variableName=k"), Iteration("K", "variableName=K"), + Iteration("l", "variableName=l"), Iteration("L", "variableName=L"), + Iteration("m", "variableName=m"), Iteration("M", "variableName=M"), + Iteration("n", "variableName=n"), Iteration("N", "variableName=N"), + Iteration("o", "variableName=o"), Iteration("O", "variableName=O"), + Iteration("p", "variableName=p"), Iteration("P", "variableName=P"), + Iteration("q", "variableName=q"), Iteration("Q", "variableName=Q"), + Iteration("r", "variableName=r"), Iteration("R", "variableName=R"), + Iteration("s", "variableName=s"), Iteration("S", "variableName=S"), + Iteration("t", "variableName=t"), Iteration("T", "variableName=T"), + Iteration("u", "variableName=u"), Iteration("U", "variableName=U"), + Iteration("v", "variableName=v"), Iteration("V", "variableName=V"), + Iteration("w", "variableName=w"), Iteration("W", "variableName=W"), + Iteration("x", "variableName=x"), Iteration("X", "variableName=X"), + Iteration("y", "variableName=y"), Iteration("Y", "variableName=Y"), + Iteration("z", "variableName=z"), Iteration("Z", "variableName=Z") + ) + fun testTokenize_variable_allLatinAlphabetCharactersAreValidVariables() { + val tokens = MathTokenizer.tokenize(variableName).toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isVariableWhoseName().isEqualTo(variableName) + } + + @Test + fun testTokenize_sqrtFunction_producesAllowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("sqrt").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("sqrt") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isTrue() + } + + @Test + fun testTokenize_sqrtFunction_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" sqrt ").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(6) + } + + @Test + fun testTokenize_expFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("exp").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("exp") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_logFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("log").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("log") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_log10Function_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("log10").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("log10") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_lnFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("ln").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("ln") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_sinFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("sin").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("sin") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_cosFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("cos").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("cos") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_tanFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("tan").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("tan") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_cotFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("cot").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("cot") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_cscFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("csc").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("csc") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_secFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("sec").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("sec") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_atanFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("atan").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("atan") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_asinFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("asin").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("asin") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_acosFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("acos").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("acos") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_absFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("abs").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("abs") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_squareRoot_producesSquareRootSymbol() { + val tokens = MathTokenizer.tokenize("√").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isSquareRootSymbol() + } + + @Test + fun testTokenize_squareRootSymbol_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" √ ").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(3) + } + + @Test + fun testTokenize_hyphen_producesMinusSymbol() { + val tokens = MathTokenizer.tokenize("-").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isMinusSymbol() + } + + @Test + fun testTokenize_mathMinusSymbol_producesMinusSymbol() { + val tokens = MathTokenizer.tokenize("−").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isMinusSymbol() + } + + @Test + fun testTokenize_minusSymbol_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" − ").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(3) + } + + @Test + fun testTokenize_plus_producesPlusSymbol() { + val tokens = MathTokenizer.tokenize("+").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isPlusSymbol() + } + + @Test + fun testTokenize_plusSymbol_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" + ").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(3) + } + + @Test + fun testTokenize_asterisk_producesMultiplySymbol() { + val tokens = MathTokenizer.tokenize("*").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isMultiplySymbol() + } + + @Test + fun testTokenize_mathTimesSymbol_producesMultiplySymbol() { + val tokens = MathTokenizer.tokenize("×").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isMultiplySymbol() + } + + @Test + fun testTokenize_multiplySymbol_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" × ").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(3) + } + + @Test + fun testTokenize_forwardSlash_producesDivideSymbol() { + val tokens = MathTokenizer.tokenize("/").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isDivideSymbol() + } + + @Test + fun testTokenize_mathDivideSymbol_producesDivideSymbol() { + val tokens = MathTokenizer.tokenize("÷").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isDivideSymbol() + } + + @Test + fun testTokenize_divideSymbol_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" ÷ ").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(3) + } + + @Test + fun testTokenize_caret_producesExponentiationSymbol() { + val tokens = MathTokenizer.tokenize("^").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isExponentiationSymbol() + } + + @Test + fun testTokenize_exponentiationSymbol_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" ^ ").toList() - private companion object { - private inline fun Token.asVerifiedType(): T { - assertThat(this).isInstanceOf(T::class.java) - return this as T - } + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(3) + } + + @Test + fun testTokenize_equals_producesEqualsSymbol() { + val tokens = MathTokenizer.tokenize("=").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isEqualsSymbol() + } + + @Test + fun testTokenize_equalsSymbol_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" = ").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(3) + } + + @Test + fun testTokenize_leftParenthesis_producesLeftParenthesisSymbol() { + val tokens = MathTokenizer.tokenize("(").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isLeftParenthesisSymbol() + } + + @Test + fun testTokenize_leftParenthesisSymbol_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" ( ").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(3) + } + + @Test + fun testTokenize_rightParenthesis_producesRightParenthesisSymbol() { + val tokens = MathTokenizer.tokenize(")").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isRightParenthesisSymbol() + } + + @Test + fun testTokenize_rightParenthesisSymbol_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" ) ").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(3) + } + + @Test + fun testTokenize_firstLetterOfFunctionNameOnly_shouldParseAsVariable() { + val tokens = MathTokenizer.tokenize("a").toList() + + // Although there are functions starting with 'a', 'a' by itself is a variable name since + // there's no context to indicate that it's part of a function name. + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isVariableWhoseName().isEqualTo("a") + } + + @Test + @RunParameterized( + Iteration("aa", "funcName=aa"), Iteration("ad", "funcName=ad"), Iteration("al", "funcName=al"), + Iteration("ca", "funcName=ca"), Iteration("ce", "funcName=ce"), Iteration("cr", "funcName=cr"), + Iteration("ea", "funcName=ea"), Iteration("ef", "funcName=ef"), Iteration("er", "funcName=er"), + Iteration("la", "funcName=la"), Iteration("lz", "funcName=lz"), Iteration("le", "funcName=le"), + Iteration("sa", "funcName=sa"), Iteration("sp", "funcName=sp"), Iteration("sz", "funcName=sz"), + Iteration("te", "funcName=te"), Iteration("to", "funcName=to"), Iteration("tr", "funcName=tr") + ) + fun testTokenize_twoVarsSharingOnlyFirstWithFunctionNames_shouldParseAsVariables() { + val tokens = MathTokenizer.tokenize(funcName).toList() + + // This test covers many cases where the first letter can be shared with function names without + // triggering a failure. Note that it doesn't cover all cases for simplicity. + assertThat(tokens).hasSize(2) + assertThat(tokens[0]).isVariableWhoseName().isNotEmpty() + assertThat(tokens[1]).isVariableWhoseName().isNotEmpty() + } + + @Test + @RunParameterized( + Iteration("ab", "funcName=ab"), Iteration("ac", "funcName=ac"), + Iteration("aco", "funcName=aco"), Iteration("as", "funcName=as"), + Iteration("asi", "funcName=asi"), Iteration("at", "funcName=at"), + Iteration("ata", "funcName=ata"), Iteration("co", "funcName=co"), + Iteration("cs", "funcName=cs"), Iteration("ex", "funcName=ex"), + Iteration("lo", "funcName=lo"), Iteration("log1", "funcName=log1"), + Iteration("se", "funcName=se"), Iteration("si", "funcName=si"), + Iteration("sq", "funcName=sq"), Iteration("ta", "funcName=ta") + ) + fun testTokenize_twoVarsSharedWithFunctionNames_shouldParseAsIncompleteFuncName() { + val tokens = MathTokenizer.tokenize(funcName).toList() + + // This test covers all cases where sharing the first few letters of a function name triggers a + // failure due to the grammar being limited to LL(1) parsing. + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isIncompleteFunctionName() + } + + @Test + fun testTokenize_sqrtWithCapitalLetters_isInterpretedAsVariables() { + val tokens = MathTokenizer.tokenize("Sqrt").toList() + + // Function names can't be capitalized, so 'Sqrt' is treated as 4 consecutive variables. + assertThat(tokens).hasSize(4) + assertThat(tokens[0]).isVariableWhoseName().isEqualTo("S") + assertThat(tokens[1]).isVariableWhoseName().isEqualTo("q") + assertThat(tokens[2]).isVariableWhoseName().isEqualTo("r") + assertThat(tokens[3]).isVariableWhoseName().isEqualTo("t") + } + + @Test + fun testTokenize_sqrtWithSpaces_isInterpretedAsVariables() { + val tokens = MathTokenizer.tokenize("s qrt").toList() + + // Spaces break the function name, so the letters must be variables. + assertThat(tokens).hasSize(4) + assertThat(tokens[0]).isVariableWhoseName().isEqualTo("s") + assertThat(tokens[1]).isVariableWhoseName().isEqualTo("q") + assertThat(tokens[2]).isVariableWhoseName().isEqualTo("r") + assertThat(tokens[3]).isVariableWhoseName().isEqualTo("t") + } + + @Test + fun testTokenize_sameTokenTwice_parsesTwice() { + val tokens = MathTokenizer.tokenize("aa").toList() + + assertThat(tokens).hasSize(2) + assertThat(tokens[0]).isVariableWhoseName().isEqualTo("a") + assertThat(tokens[1]).isVariableWhoseName().isEqualTo("a") + } + + @Test + fun testTokenize_exclamationPoint_producesInvalidToken() { + val tokens = MathTokenizer.tokenize("!").toList() + + // '!' is not yet supported by the tokenizer, so it's an invalid token. + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isInvalidToken() + } + + @Test + @RunParameterized( + Iteration("α", "token=α"), Iteration("Α", "token=Α"), + Iteration("β", "token=β"), Iteration("Β", "token=Β"), + Iteration("γ", "token=γ"), Iteration("Γ", "token=Γ"), + Iteration("δ", "token=δ"), Iteration("Δ", "token=Δ"), + Iteration("ϵ", "token=ϵ"), Iteration("Ε", "token=Ε"), + Iteration("ζ", "token=ζ"), Iteration("Ζ", "token=Ζ"), + Iteration("η", "token=η"), Iteration("Η", "token=Η"), + Iteration("θ", "token=θ"), Iteration("Θ", "token=Θ"), + Iteration("ι", "token=ι"), Iteration("Ι", "token=Ι"), + Iteration("κ", "token=κ"), Iteration("Κ", "token=Κ"), + Iteration("λ", "token=λ"), Iteration("Λ", "token=Λ"), + Iteration("μ", "token=μ"), Iteration("Μ", "token=Μ"), + Iteration("ν", "token=ν"), Iteration("Ν", "token=Ν"), + Iteration("ξ", "token=ξ"), Iteration("Ξ", "token=Ξ"), + Iteration("ο", "token=ο"), Iteration("Ο", "token=Ο"), + Iteration("π", "token=π"), Iteration("Π", "token=Π"), + Iteration("ρ", "token=ρ"), Iteration("Ρ", "token=Ρ"), + Iteration("σ", "token=σ"), Iteration("Σ", "token=Σ"), + Iteration("τ", "token=τ"), Iteration("Τ", "token=Τ"), + Iteration("υ", "token=υ"), Iteration("Υ", "token=Υ"), + Iteration("ϕ", "token=ϕ"), Iteration("Φ", "token=Φ"), + Iteration("χ", "token=χ"), Iteration("Χ", "token=Χ"), + Iteration("ψ", "token=ψ"), Iteration("Ψ", "token=Ψ"), + Iteration("ω", "token=ω"), Iteration("Ω", "token=Ω"), + Iteration("ς", "token=ς") + ) + fun testTokenize_greekLetters_produceInvalidTokens() { + val tokens = MathTokenizer.tokenize(token).toList() + + // Greek letters are not yet supported. + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isInvalidToken() + } + + @Test + fun testTokenize_manyOtherUnicodeValues_produceInvalidTokens() { + // Build a large list of unicode characters minus those which are actually allowed. The ASCII + // range is excluded from this list. + val characters = ('\u007f' .. '\uffff').filterNot { + it in listOf('×', '÷', '−', '√') } + val charStr = characters.joinToString("") + + val tokens = MathTokenizer.tokenize(charStr).toList() + + // Verify that all of the unicode characters cover in this range are invalid. + assertThat(tokens).hasSize(charStr.length) + tokens.forEach { assertThat(it).isInvalidToken() } + // Sanity check to ensure that the tokens are actually populated. + assertThat(tokens.size).isGreaterThan(0x7fff) } - private companion object { - private fun assertThat(actual: T): TokenSubject = - assertAbout(createTokenSubjectFactory()).that(actual) + @Test + fun testTokenize_validAndInvalidTokens_tokenizerContinues() { + val tokens = MathTokenizer.tokenize("2*7!/|-9|").toList() + + assertThat(tokens).hasSize(9) + assertThat(tokens[0]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens[1]).isMultiplySymbol() + assertThat(tokens[2]).isPositiveIntegerWhoseValue().isEqualTo(7) + assertThat(tokens[3]).isInvalidToken() + assertThat(tokens[4]).isDivideSymbol() + assertThat(tokens[5]).isInvalidToken() + assertThat(tokens[6]).isMinusSymbol() + assertThat(tokens[7]).isPositiveIntegerWhoseValue().isEqualTo(9) + assertThat(tokens[8]).isInvalidToken() + } + + @Test + fun testTokenize_manyTokenTypes_parseCorrectlyAndInOrder() { + val tokens = MathTokenizer.tokenize("1 * (√2 - 3.14) + 4^7-8/3×-2 + sqrt(7)÷3").toList() + + assertThat(tokens).hasSize(26) + assertThat(tokens[0]).isPositiveIntegerWhoseValue().isEqualTo(1) + assertThat(tokens[1]).isMultiplySymbol() + assertThat(tokens[2]).isLeftParenthesisSymbol() + assertThat(tokens[3]).isSquareRootSymbol() + assertThat(tokens[4]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens[5]).isMinusSymbol() + assertThat(tokens[6]).isPositiveRealNumberWhoseValue().isWithin(1e-5).of(3.14) + assertThat(tokens[7]).isRightParenthesisSymbol() + assertThat(tokens[8]).isPlusSymbol() + assertThat(tokens[9]).isPositiveIntegerWhoseValue().isEqualTo(4) + assertThat(tokens[10]).isExponentiationSymbol() + assertThat(tokens[11]).isPositiveIntegerWhoseValue().isEqualTo(7) + assertThat(tokens[12]).isMinusSymbol() + assertThat(tokens[13]).isPositiveIntegerWhoseValue().isEqualTo(8) + assertThat(tokens[14]).isDivideSymbol() + assertThat(tokens[15]).isPositiveIntegerWhoseValue().isEqualTo(3) + assertThat(tokens[16]).isMultiplySymbol() + assertThat(tokens[17]).isMinusSymbol() + assertThat(tokens[18]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens[19]).isPlusSymbol() + assertThat(tokens[20]).isFunctionNameThat().hasNameThat().isEqualTo("sqrt") + assertThat(tokens[21]).isLeftParenthesisSymbol() + assertThat(tokens[22]).isPositiveIntegerWhoseValue().isEqualTo(7) + assertThat(tokens[23]).isRightParenthesisSymbol() + assertThat(tokens[24]).isDivideSymbol() + assertThat(tokens[25]).isPositiveIntegerWhoseValue().isEqualTo(3) + } + + @Test + fun testTokenize_allFormsOfWhiteSpaceAreIgnored() { + val tokens = MathTokenizer.tokenize(" \n\t2\r\n 3 \n").toList() - private fun createTokenSubjectFactory() = - Subject.Factory, T>(::TokenSubject) + assertThat(tokens).hasSize(2) + assertThat(tokens[0]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens[1]).isPositiveIntegerWhoseValue().isEqualTo(3) } } diff --git a/utility/src/test/java/org/oppia/android/util/math/PeekableIteratorTest.kt b/utility/src/test/java/org/oppia/android/util/math/PeekableIteratorTest.kt new file mode 100644 index 00000000000..96da81d6baa --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/PeekableIteratorTest.kt @@ -0,0 +1,600 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import java.util.function.Supplier +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.Mockito.reset +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.stubbing.Answer +import org.oppia.android.testing.assertThrows +import org.oppia.android.util.math.PeekableIterator.Companion.toPeekableIterator +import org.robolectric.annotation.LooperMode + +/** Tests for [PeekableIterator]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class PeekableIteratorTest { + @Rule + @JvmField + val mockitoRule: MockitoRule = MockitoJUnit.rule() + + @Mock lateinit var mockSequenceSupplier: Supplier + + @Test + fun testHasNext_emptySequence_returnsFalse() { + val sequence = sequenceOf() + val iterator = sequence.toPeekableIterator() + + val hasNext = iterator.hasNext() + + assertThat(hasNext).isFalse() + } + + @Test + fun testHasNext_singletonSequence_returnsTrue() { + val sequence = sequenceOf("element") + val iterator = sequence.toPeekableIterator() + + val hasNext = iterator.hasNext() + + assertThat(hasNext).isTrue() + } + + @Test + fun testNext_emptySequence_throwsException() { + val sequence = sequenceOf() + val iterator = sequence.toPeekableIterator() + + assertThrows(NoSuchElementException::class) { iterator.next() } + } + + @Test + fun testNext_singletonSequence_returnsValue() { + val sequence = sequenceOf("element") + val iterator = sequence.toPeekableIterator() + + val value = iterator.next() + + assertThat(value).isEqualTo("element") + } + + @Test + fun testNext_multipleCalls_multiElementSequence_returnsAllValues() { + val sequence = sequenceOf("first", "second", "third") + val iterator = sequence.toPeekableIterator() + + val value1 = iterator.next() + val value2 = iterator.next() + val value3 = iterator.next() + + assertThat(value1).isEqualTo("first") + assertThat(value2).isEqualTo("second") + assertThat(value3).isEqualTo("third") + } + + @Test + fun testHasNext_singletonSequence_afterNext_returnsFalse() { + val sequence = sequenceOf("element") + val iterator = sequence.toPeekableIterator() + iterator.next() + + val hasNext = iterator.hasNext() + + assertThat(hasNext).isFalse() + } + + @Test + fun testHasNext_multiElementSequence_afterNext_returnsTrue() { + val sequence = sequenceOf("first", "second", "third") + val iterator = sequence.toPeekableIterator() + iterator.next() + + val hasNext = iterator.hasNext() + + assertThat(hasNext).isTrue() + } + + @Test + fun testAsIterator_emptySequence_convertsToEmptyList() { + val sequence = sequenceOf() + val iterator = sequence.toPeekableIterator() + + val list = iterator.toList() + + assertThat(list).isEmpty() + } + + @Test + fun testAsIterator_singletonSequence_convertsToSingletonList() { + val sequence = sequenceOf("element") + val iterator = sequence.toPeekableIterator() + + val list = iterator.toList() + + assertThat(list).containsExactly("element") + } + + @Test + fun testAsIterator_multiElementSequence_convertsToList() { + val sequence = sequenceOf("first", "second", "third") + val iterator = sequence.toPeekableIterator() + + val list = iterator.toList() + + assertThat(list).containsExactly("first", "second", "third").inOrder() + } + + @Test + fun testHasNext_multiElementSequence_convertedToList_returnsFalse() { + val sequence = sequenceOf("first", "second", "third") + val iterator = sequence.toPeekableIterator() + iterator.toList() + + val hasNext = iterator.hasNext() + + // No elements remain after converting the iterator to a list (since it should be fully + // consumed). + assertThat(hasNext).isFalse() + } + + @Test + fun testPeek_emptySequence_returnsNull() { + val sequence = sequenceOf() + val iterator = sequence.toPeekableIterator() + + val nextElement = iterator.peek() + + assertThat(nextElement).isNull() + } + + @Test + fun testPeek_emptySequence_twice_returnsNull() { + val sequence = sequenceOf() + val iterator = sequence.toPeekableIterator() + iterator.peek() + + // Peek a second time. + val nextElement = iterator.peek() + + assertThat(nextElement).isNull() + } + + @Test + fun testPeek_singletonSequence_returnsElement() { + val sequence = sequenceOf("element") + val iterator = sequence.toPeekableIterator() + + val nextElement = iterator.peek() + + assertThat(nextElement).isEqualTo("element") + } + + @Test + fun testPeek_singletonSequence_twice_returnsElement() { + val sequence = sequenceOf("element") + val iterator = sequence.toPeekableIterator() + iterator.peek() + + // Peek a second time. + val nextElement = iterator.peek() + + assertThat(nextElement).isEqualTo("element") + } + + @Test + fun testPeek_singletonSequence_afterNext_returnsNull() { + val sequence = sequenceOf("element") + val iterator = sequence.toPeekableIterator() + iterator.next() + + val nextElement = iterator.peek() + + // There is no longer a next element since it was consumed. + assertThat(nextElement).isNull() + } + + @Test + fun testPeek_singletonSequence_afterNext_twice_returnsNull() { + val sequence = sequenceOf("element") + val iterator = sequence.toPeekableIterator() + iterator.next() + iterator.peek() + + // Peek a second time after consuming the element. + val nextElement = iterator.peek() + + // It's still missing. + assertThat(nextElement).isNull() + } + + @Test + fun testPeek_singletonSequence_peekThenNext_returnsElementFromBoth() { + val sequence = sequenceOf("element") + val iterator = sequence.toPeekableIterator() + + val nextElement = iterator.peek() + val consumedElement = iterator.next() + + // Both functions should return the same value in this order. + assertThat(nextElement).isEqualTo("element") + assertThat(consumedElement).isEqualTo("element") + } + + @Test + fun testExpectNextValue_emptySequence_returnsNull() { + val sequence = sequenceOf() + val iterator = sequence.toPeekableIterator() + + val matchedValue = iterator.expectNextValue { "does not match" } + + // No values to match. + assertThat(matchedValue).isNull() + } + + @Test + fun testExpectNextValue_valueMatches_returnsValue() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + + val matchedValue = iterator.expectNextValue { "matches" } + + assertThat(matchedValue).isEqualTo("matches") + } + + @Test + fun testHasNext_afterExpectNextValue_valueMatches_returnsFalse() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + iterator.expectNextValue { "matches" } + + val hasNext = iterator.hasNext() + + // No other elements since the only one was consumed. + assertThat(hasNext).isFalse() + } + + @Test + fun testPeek_afterExpectNextValue_valueMatches_returnsNull() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + iterator.expectNextValue { "matches" } + + val nextElement = iterator.peek() + + // No other elements since the only one was consumed. + assertThat(nextElement).isNull() + } + + @Test + fun testExpectNextValue_valueDoesNotMatch_returnsNull() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + + val matchedValue = iterator.expectNextValue { "does not match" } + + // No values to match. + assertThat(matchedValue).isNull() + } + + @Test + fun testHasNext_afterExpectNextValue_valueDoesNotMatch_returnsTrue() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + iterator.expectNextValue { "does not match" } + + val hasNext = iterator.hasNext() + + // The element is still present. + assertThat(hasNext).isTrue() + } + + @Test + fun testPeek_afterExpectNextValue_valueDoesNotMatch_returnsElement() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + iterator.expectNextValue { "does not match" } + + val nextElement = iterator.next() + + // The element is still present. + assertThat(nextElement).isEqualTo("matches") + } + + @Test + fun testExpectNextMatches_emptySequence_returnsNull() { + val sequence = sequenceOf() + val iterator = sequence.toPeekableIterator() + + val matchedValue = iterator.expectNextMatches { true } + + // No values to match. + assertThat(matchedValue).isNull() + } + + @Test + fun testExpectNextMatches_valueMatches_returnsValue() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + + val matchedValue = iterator.expectNextMatches { true } + + assertThat(matchedValue).isEqualTo("matches") + } + + @Test + fun testHasNext_afterExpectNextMatches_valueMatches_returnsFalse() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + iterator.expectNextMatches { true } + + val hasNext = iterator.hasNext() + + // No other elements since the only one was consumed. + assertThat(hasNext).isFalse() + } + + @Test + fun testPeek_afterExpectNextMatches_valueMatches_returnsNull() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + iterator.expectNextMatches { true } + + val nextElement = iterator.peek() + + // No other elements since the only one was consumed. + assertThat(nextElement).isNull() + } + + @Test + fun testExpectNextMatches_valueDoesNotMatch_returnsNull() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + + val matchedValue = iterator.expectNextMatches { false } + + // The predicate didn't match. + assertThat(matchedValue).isNull() + } + + @Test + fun testHasNext_afterExpectNextMatches_valueDoesNotMatch_returnsTrue() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + iterator.expectNextMatches { false } + + val hasNext = iterator.hasNext() + + // The element is still present. + assertThat(hasNext).isTrue() + } + + @Test + fun testPeek_afterExpectNextMatches_valueDoesNotMatch_returnsElement() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + iterator.expectNextMatches { false } + + val nextElement = iterator.peek() + + // The element is still present. + assertThat(nextElement).isEqualTo("matches") + } + + @Test + fun testGetRetrievalCount_emptySequence_returnsZero() { + val sequence = sequenceOf() + val iterator = sequence.toPeekableIterator() + + val retrievalCount = iterator.getRetrievalCount() + + assertThat(retrievalCount).isEqualTo(0) + } + + @Test + fun testGetRetrievalCount_singletonSequence_returnsZero() { + val sequence = sequenceOf("element") + val iterator = sequence.toPeekableIterator() + + val retrievalCount = iterator.getRetrievalCount() + + assertThat(retrievalCount).isEqualTo(0) + } + + @Test + fun testGetRetrievalCount_afterNext_singletonSequence_returnsOne() { + val sequence = sequenceOf("element") + val iterator = sequence.toPeekableIterator() + iterator.next() + + val retrievalCount = iterator.getRetrievalCount() + + // One element was removed. + assertThat(retrievalCount).isEqualTo(1) + } + + @Test + fun testGetRetrievalCount_afterPeek_singletonSequence_returnsZero() { + val sequence = sequenceOf("element") + val iterator = sequence.toPeekableIterator() + iterator.peek() + + val retrievalCount = iterator.getRetrievalCount() + + // Peek does not remove the element. + assertThat(retrievalCount).isEqualTo(0) + } + + @Test + fun testGetRetrievalCount_afterMatchingExpectNextValue_returnsOne() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + iterator.expectNextValue { "matches" } + + val retrievalCount = iterator.getRetrievalCount() + + // One element was removed due to the match. + assertThat(retrievalCount).isEqualTo(1) + } + + @Test + fun testGetRetrievalCount_afterFailingExpectNextValue_returnsZero() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + iterator.expectNextValue { "does not match" } + + val retrievalCount = iterator.getRetrievalCount() + + // No elements were removed since nothing matched. + assertThat(retrievalCount).isEqualTo(0) + } + + @Test + fun testGetRetrievalCount_afterMatchingExpectNextMatches_returnsOne() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + iterator.expectNextMatches { true } + + val retrievalCount = iterator.getRetrievalCount() + + // One element was removed due to the match. + assertThat(retrievalCount).isEqualTo(1) + } + + @Test + fun testGetRetrievalCount_afterFailingExpectNextMatches_returnsZero() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + iterator.expectNextMatches { false } + + val retrievalCount = iterator.getRetrievalCount() + + // No elements were removed since nothing matched. + assertThat(retrievalCount).isEqualTo(0) + } + + @Test + fun testGetRetrievalCount_afterMultipleNext_returnsNextCount() { + val sequence = sequenceOf("first", "second", "third") + val iterator = sequence.toPeekableIterator() + // Call next() twice. + iterator.next() + iterator.next() + + val retrievalCount = iterator.getRetrievalCount() + + // The number of consumed elements from the iterator should be returned. + assertThat(retrievalCount).isEqualTo(2) + } + + @Test + fun testGetRetrievalCount_afterConvertingToList_returnsListSize() { + val sequence = sequenceOf("first", "second", "third", "fourth", "fifth") + val iterator = sequence.toPeekableIterator() + val elements = iterator.toList() + + val retrievalCount = iterator.getRetrievalCount() + + // Since the iterator was fully consumed, the retrieval count should be the same as the list + // size. + assertThat(retrievalCount).isEqualTo(5) + assertThat(elements.size).isEqualTo(retrievalCount) + } + + @Test + fun testCreateIterator_doesNotConsumeElementsFromSequence() { + val generatedSequence = createGeneratingSequence() + + generatedSequence.toPeekableIterator() + + // The sequence is never called just upon iterator creation. + verifyNoMoreInteractions(mockSequenceSupplier) + } + + @Test + fun testPeek_consumesElementFromSequence() { + val iterator = createGeneratingSequence().toPeekableIterator() + + iterator.peek() + + // The first peek must consume one element in order to populate it. + verify(mockSequenceSupplier).get() + } + + @Test + fun testPeek_twice_doesNotConsumeAdditionalElementFromSequence() { + val iterator = createGeneratingSequence().toPeekableIterator() + iterator.peek() + reset(mockSequenceSupplier) + + // Peek a second time. + iterator.peek() + + // The second peek doesn't consume an element (since the iterator's contract is to never look + // more than 1 element ahead). + verifyNoMoreInteractions(mockSequenceSupplier) + } + + @Test + fun testNext_consumesOneElementFromSequence() { + val iterator = createGeneratingSequence().toPeekableIterator() + + iterator.next() + + // The sequence should have only one value retrieved due to the next() call. + verify(mockSequenceSupplier).get() + } + + @Test + fun testNext_twice_consumesTwoElementsFromSequence() { + val iterator = createGeneratingSequence().toPeekableIterator() + + // Iterate two items. + iterator.next() + iterator.next() + + // One value should be retrieved from the sequence for each next() call. + verify(mockSequenceSupplier, times(2)).get() + } + + @Test + fun testConvertToList_consumesAllElementsFromSequence() { + val generatedSequence = createGeneratingSequence() + val iterator = generatedSequence.toPeekableIterator() + + val list = iterator.toList() + + // The whole sequence should be consumed through the iterator when converting it to a list. Note + // the extra call to get() is for the final element that indicates the sequence has ended per + // generateSequence. + verify(mockSequenceSupplier, times(list.size + 1)).get() + assertThat(list).isNotEmpty() + } + + private fun createGeneratingSequence(): Sequence { + `when`(mockSequenceSupplier.get()).thenReturn("string0", "string1", "string2", "string3", null) + return generateSequence { mockSequenceSupplier.get() } + } + + private companion object { + /** + * Returns a [List] that contains all elements from the [Iterator] (i.e. the iterator is fully + * consumed). + */ + private fun Iterator.toList(): List { + return mutableListOf().apply { + this@toList.forEach(this::add) + } + } + } +} From 3fa8dadc59d9778955c3579dc8a88c5f39c6974e Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 20 Jan 2022 22:41:01 -0800 Subject: [PATCH 048/134] Lint fixes. This includes a fix for 'fun interface' not working with ktlint (see #4122). --- .../junit/OppiaParameterizedTestRunner.kt | 13 ++-- .../android/testing/junit/ParameterValue.kt | 75 +++++++++++++------ .../ParameterizedAndroidJUnit4ClassRunner.kt | 2 +- .../ParameterizedRobolectricTestRunner.kt | 4 +- .../junit/ParameterizedRunnerDelegate.kt | 2 +- .../oppia/android/util/math/MathTokenizer.kt | 2 +- .../android/util/math/PeekableIterator.kt | 4 +- .../android/util/math/MathTokenizerTest.kt | 2 +- .../android/util/math/PeekableIteratorTest.kt | 5 +- 9 files changed, 69 insertions(+), 40 deletions(-) diff --git a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt index 3579bc95d5b..e237553a9fc 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt @@ -1,8 +1,5 @@ package org.oppia.android.testing.junit -import java.lang.annotation.Repeatable -import java.lang.reflect.Field -import java.lang.reflect.Method import org.junit.runner.Description import org.junit.runner.Runner import org.junit.runner.manipulation.Filter @@ -11,6 +8,9 @@ import org.junit.runner.manipulation.Sortable import org.junit.runner.manipulation.Sorter import org.junit.runner.notification.RunNotifier import org.junit.runners.Suite +import java.lang.annotation.Repeatable +import java.lang.reflect.Field +import java.lang.reflect.Method /** * JUnit test runner that enables support for parameterization, that is, running a single test @@ -71,7 +71,7 @@ import org.junit.runners.Suite * contain (thus they should be treated as undefined outside of tests that specific define their * value via [Iteration]). */ -class OppiaParameterizedTestRunner(private val testClass: Class<*>): Suite(testClass, listOf()) { +class OppiaParameterizedTestRunner(private val testClass: Class<*>) : Suite(testClass, listOf()) { private val parameterizedMethods = computeParameterizedMethods() private val childrenRunners by lazy { // Collect all parameterized methods (for each iteration they support) plus one test runner for @@ -239,7 +239,8 @@ class OppiaParameterizedTestRunner(private val testClass: Class<*>): Suite(testC annotation class Iteration(val name: String, vararg val keyValuePairs: String) private data class ParameterizedMethodDeclaration( - val method: Method, val rawValues: Map> + val method: Method, + val rawValues: Map> ) private class ProxyParameterizedTestRunner( @@ -247,7 +248,7 @@ class OppiaParameterizedTestRunner(private val testClass: Class<*>): Suite(testC private val parameterizedMethods: Map, private val methodName: String?, private val iterationName: String? = null - ): Runner(), Filterable, Sortable { + ) : Runner(), Filterable, Sortable { private val delegate by lazy { constructDelegate() } private val delegateRunner by lazy { checkNotNull(delegate as? Runner) { "Delegate runner isn't a JUnit runner: $delegate" } diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt index 360a44649e0..fc113db4674 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt @@ -12,8 +12,9 @@ import java.lang.reflect.Field */ internal sealed class ParameterValue(val key: String, val value: Any) { private class BooleanParameterValue private constructor( - key: String, value: Boolean - ): ParameterValue(key, value) { + key: String, + value: Boolean + ) : ParameterValue(key, value) { companion object { /** * Returns a new [ParameterValue] for the specified [key] and a [Boolean] parsed @@ -35,8 +36,9 @@ internal sealed class ParameterValue(val key: String, val value: Any) { } private class IntParameterValue private constructor( - key: String, value: Int - ): ParameterValue(key, value) { + key: String, + value: Int + ) : ParameterValue(key, value) { companion object { /** * Returns a new [ParameterValue] for the specified [key] and an [Int] parsed representation @@ -49,8 +51,9 @@ internal sealed class ParameterValue(val key: String, val value: Any) { } private class LongParameterValue private constructor( - key: String, value: Long - ): ParameterValue(key, value) { + key: String, + value: Long + ) : ParameterValue(key, value) { companion object { /** * Returns a new [ParameterValue] for the specified [key] and a [Long] parsed representation @@ -63,8 +66,9 @@ internal sealed class ParameterValue(val key: String, val value: Any) { } private class FloatParameterValue private constructor( - key: String, value: Float - ): ParameterValue(key, value) { + key: String, + value: Float + ) : ParameterValue(key, value) { companion object { /** * Returns a new [ParameterValue] for the specified [key] and a [Float] parsed representation @@ -77,8 +81,9 @@ internal sealed class ParameterValue(val key: String, val value: Any) { } private class DoubleParameterValue private constructor( - key: String, value: Double - ): ParameterValue(key, value) { + key: String, + value: Double + ) : ParameterValue(key, value) { companion object { /** * Returns a new [ParameterValue] for the specified [key] and a [Double] parsed representation @@ -91,8 +96,9 @@ internal sealed class ParameterValue(val key: String, val value: Any) { } private class StringParameterValue private constructor( - key: String, value: String - ): ParameterValue(key, value) { + key: String, + value: String + ) : ParameterValue(key, value) { companion object { /** * Returns a new [ParameterValue] for the specified [key] and a [String] parsed representation @@ -104,12 +110,36 @@ internal sealed class ParameterValue(val key: String, val value: Any) { } internal companion object { - private val booleanValueParser = createParser(BooleanParameterValue::createParameter) - private val intValueParser = createParser(IntParameterValue::createParameter) - private val longValueParser = createParser(LongParameterValue::createParameter) - private val floatValueParser = createParser(FloatParameterValue::createParameter) - private val doubleValueParser = createParser(DoubleParameterValue::createParameter) - private val stringValueParser = createParser(StringParameterValue::createParameter) + private val booleanValueParser = object : ParameterValueParser { + override fun parseParameter(key: String, rawValue: String): ParameterValue? { + return BooleanParameterValue.createParameter(key, rawValue) + } + } + private val intValueParser = object : ParameterValueParser { + override fun parseParameter(key: String, rawValue: String): ParameterValue? { + return IntParameterValue.createParameter(key, rawValue) + } + } + private val longValueParser = object : ParameterValueParser { + override fun parseParameter(key: String, rawValue: String): ParameterValue? { + return LongParameterValue.createParameter(key, rawValue) + } + } + private val floatValueParser = object : ParameterValueParser { + override fun parseParameter(key: String, rawValue: String): ParameterValue? { + return FloatParameterValue.createParameter(key, rawValue) + } + } + private val doubleValueParser = object : ParameterValueParser { + override fun parseParameter(key: String, rawValue: String): ParameterValue? { + return DoubleParameterValue.createParameter(key, rawValue) + } + } + private val stringValueParser = object : ParameterValueParser { + override fun parseParameter(key: String, rawValue: String): ParameterValue { + return StringParameterValue.createParameter(key, rawValue) + } + } /** * Returns a new [ParameterValueParser] corresponding to the type of the specified [field], or @@ -127,17 +157,16 @@ internal sealed class ParameterValue(val key: String, val value: Any) { } } + // TODO(#4122): Use 'fun interface' here, instead, once ktlint supports it. This allows for + // method references to be passed to createParser when defining the parsers above. See the + // blame PR's change for this code for a commit that has the functional interface alternative. /** A string parser for a specific [ParameterValue] type. */ - fun interface ParameterValueParser { + interface ParameterValueParser { // ktlint-disable /** * Returns a [ParameterValue] corresponding to the specified [key], and with a type-safe * parsing of [rawValue], or null if the string value is invalid. */ fun parseParameter(key: String, rawValue: String): ParameterValue? } - - // A hack to work around the fact that Kotlin doesn't support assignment conversion from - // references to a functional interface. - private fun createParser(parser: ParameterValueParser) = parser } } diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt index fcb642a437c..8526c4b557d 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt @@ -14,7 +14,7 @@ internal class ParameterizedAndroidJUnit4ClassRunner( private val parameterizedMethods: Map, private val methodName: String?, private val iterationName: String? -): AndroidJUnit4ClassRunner(testClass), ParameterizedRunnerOverrideMethods { +) : AndroidJUnit4ClassRunner(testClass), ParameterizedRunnerOverrideMethods { private val delegate by lazy { ParameterizedRunnerDelegate( parameterizedMethods, diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt index 1a6df490125..161a4bce0c2 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt @@ -14,7 +14,7 @@ internal class ParameterizedRobolectricTestRunner( private val parameterizedMethods: Map, private val methodName: String?, private val iterationName: String? -): RobolectricTestRunner(testClass), ParameterizedRunnerOverrideMethods { +) : RobolectricTestRunner(testClass), ParameterizedRunnerOverrideMethods { private val delegate by lazy { ParameterizedRunnerDelegate( parameterizedMethods, @@ -37,7 +37,7 @@ internal class ParameterizedRobolectricTestRunner( override fun getHelperTestRunner( bootstrappedTestClass: Class<*>? ): HelperTestRunner { - return object: HelperTestRunner(bootstrappedTestClass) { + return object : HelperTestRunner(bootstrappedTestClass) { override fun methodInvoker(method: FrameworkMethod?, test: Any?): Statement { delegate.fetchMethodInvokerFromParent = { innerMethod, innerParent -> super.methodInvoker(innerMethod, innerParent) diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt index 02d90dbd5d8..fc6c469aae9 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt @@ -13,7 +13,7 @@ internal class ParameterizedRunnerDelegate( private val parameterizedMethods: Map, private val methodName: String?, private val iterationName: String? -): ParameterizedRunnerOverrideMethods { +) : ParameterizedRunnerOverrideMethods { /** * A lambda used to call into the parent runner's [getChildren] method. This should be set by * helper parameterized test runners. diff --git a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt index b710ae7908b..d45ce14d571 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt @@ -9,8 +9,8 @@ import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT import org.oppia.android.app.model.MathUnaryOperation import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE -import java.lang.StringBuilder import org.oppia.android.util.math.PeekableIterator.Companion.toPeekableIterator +import java.lang.StringBuilder /** * Input tokenizer for math (numeric & algebraic) expressions and equations. diff --git a/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt b/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt index 4d752fdc2f1..8ecef1499bb 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt @@ -14,7 +14,7 @@ package org.oppia.android.util.math * * This class is not safe to use across multiple threads and requires synchronization. */ -class PeekableIterator private constructor( +class PeekableIterator private constructor( private val backingIterator: Iterator ) : Iterator { private var next: T? = null @@ -84,7 +84,7 @@ class PeekableIterator private constructor( * [List]). Some sequences do support multiple iteration, so the exact behavior of the returned * iterator will be sequence implementation dependent. */ - fun Sequence.toPeekableIterator(): PeekableIterator = + fun Sequence.toPeekableIterator(): PeekableIterator = PeekableIterator(iterator()) } } diff --git a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt index 276911cb62a..a91ea971626 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt @@ -644,7 +644,7 @@ class MathTokenizerTest { fun testTokenize_manyOtherUnicodeValues_produceInvalidTokens() { // Build a large list of unicode characters minus those which are actually allowed. The ASCII // range is excluded from this list. - val characters = ('\u007f' .. '\uffff').filterNot { + val characters = ('\u007f'..'\uffff').filterNot { it in listOf('×', '÷', '−', '√') } val charStr = characters.joinToString("") diff --git a/utility/src/test/java/org/oppia/android/util/math/PeekableIteratorTest.kt b/utility/src/test/java/org/oppia/android/util/math/PeekableIteratorTest.kt index 96da81d6baa..0b6cddc7dbe 100644 --- a/utility/src/test/java/org/oppia/android/util/math/PeekableIteratorTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/PeekableIteratorTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.util.math import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat -import java.util.function.Supplier import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -14,10 +13,10 @@ import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule -import org.mockito.stubbing.Answer import org.oppia.android.testing.assertThrows import org.oppia.android.util.math.PeekableIterator.Companion.toPeekableIterator import org.robolectric.annotation.LooperMode +import java.util.function.Supplier /** Tests for [PeekableIterator]. */ // FunctionName: test names are conventionally named with underscores. @@ -56,7 +55,7 @@ class PeekableIteratorTest { val sequence = sequenceOf() val iterator = sequence.toPeekableIterator() - assertThrows(NoSuchElementException::class) { iterator.next() } + assertThrows(NoSuchElementException::class) { iterator.next() } } @Test From 87a41db6524a0f7fd9a8ad4b04764784402ccc76 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 20 Jan 2022 22:55:59 -0800 Subject: [PATCH 049/134] Remove internals that broke things. --- .../java/org/oppia/android/testing/junit/ParameterValue.kt | 4 ++-- .../org/oppia/android/testing/junit/ParameterizedMethod.kt | 4 ++-- .../android/testing/junit/ParameterizedRunnerDelegate.kt | 2 +- .../testing/junit/ParameterizedRunnerOverrideMethods.kt | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt index fc113db4674..a389a338518 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt @@ -10,7 +10,7 @@ import java.lang.reflect.Field * @property value the type-correct value to assign to the field prior to executing the iteration * corresponding to this value */ -internal sealed class ParameterValue(val key: String, val value: Any) { +sealed class ParameterValue(val key: String, val value: Any) { private class BooleanParameterValue private constructor( key: String, value: Boolean @@ -109,7 +109,7 @@ internal sealed class ParameterValue(val key: String, val value: Any) { } } - internal companion object { + companion object { private val booleanValueParser = object : ParameterValueParser { override fun parseParameter(key: String, rawValue: String): ParameterValue? { return BooleanParameterValue.createParameter(key, rawValue) diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedMethod.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedMethod.kt index d56cb4e3e87..e9de6ce70f7 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedMethod.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedMethod.kt @@ -9,7 +9,7 @@ import java.util.Locale * * @property methodName the name of the test method that's been parameterized */ -internal class ParameterizedMethod( +class ParameterizedMethod( val methodName: String, private val values: Map>, private val parameterFields: List @@ -25,7 +25,7 @@ internal class ParameterizedMethod( * method is called for each iteration (since the test method should be executed multiples, once * for each of its iteration). */ - internal fun initializeForTest(testClassInstance: Any, iterationName: String) { + fun initializeForTest(testClassInstance: Any, iterationName: String) { // Retrieve the setters for the fields (since these are expected to be used instead of direct // property access in Kotlin). Note that these need to be re-fetched since the instance class // may change (due to Robolectric instrumentation including custom class loading & bytecode diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt index fc6c469aae9..dc9699b31d5 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt @@ -9,7 +9,7 @@ import org.junit.runners.model.Statement * This class performs the actual field injection and execution delegation for running each * parameterized test method. */ -internal class ParameterizedRunnerDelegate( +class ParameterizedRunnerDelegate( private val parameterizedMethods: Map, private val methodName: String?, private val iterationName: String? diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerOverrideMethods.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerOverrideMethods.kt index 6d591ec274f..890685fd674 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerOverrideMethods.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerOverrideMethods.kt @@ -6,7 +6,7 @@ import org.junit.runners.model.Statement /** * Specifies methods that the helper parameterized runners should override from JUnit's test runner. */ -internal interface ParameterizedRunnerOverrideMethods { +interface ParameterizedRunnerOverrideMethods { /** See [org.junit.runners.BlockJUnit4ClassRunner.getChildren]. */ fun getChildren(): MutableList From c0172ceff6be19d63e3da63f0218faab8f7d6fc8 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 21 Jan 2022 11:45:17 -0800 Subject: [PATCH 050/134] Add regex exemptions. --- scripts/assets/file_content_validation_checks.textproto | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index 5bef515e6df..ee0fc5331f6 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -119,6 +119,7 @@ file_content_checks { exempted_file_name: "domain/src/main/java/org/oppia/android/domain/util/WorkDataExtensions.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/onboarding/AppStartupStateControllerTest.kt" exempted_file_name: "testing/src/main/java/org/oppia/android/testing/OppiaTestRunner.kt" + exempted_file_name: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedMethod.kt" exempted_file_name: "testing/src/main/java/org/oppia/android/testing/time/FakeOppiaClock.kt" exempted_file_name: "utility/src/main/java/org/oppia/android/util/extensions/BundleExtensions.kt" exempted_file_name: "utility/src/main/java/org/oppia/android/util/locale/MachineLocaleImpl.kt" @@ -242,6 +243,7 @@ file_content_checks { exempted_file_name: "app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt" exempted_file_name: "app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt" exempted_file_name: "testing/src/main/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRule.kt" + exempted_file_name: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedMethod.kt" exempted_file_name: "testing/src/main/java/org/oppia/android/testing/robolectric/ShadowBidiFormatter.kt" exempted_file_name: "testing/src/test/java/org/oppia/android/testing/robolectric/ShadowBidiFormatterTest.kt" exempted_file_name: "utility/src/main/java/org/oppia/android/util/logging/firebase/FirebaseEventLogger.kt" From ff41eebf63547cb7536d8b9c3b026ee99e309691 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 21 Jan 2022 12:13:38 -0800 Subject: [PATCH 051/134] Post-merge fix. --- .../java/org/oppia/android/util/math/MathExpressionParser.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt index ebebe862430..688ba4408fd 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -64,6 +64,7 @@ import org.oppia.android.util.math.MathTokenizer.Companion.Token.VariableName import kotlin.math.absoluteValue import org.oppia.android.util.math.MathParsingError.EquationHasTooManyEqualsError import org.oppia.android.util.math.MathParsingError.EquationIsMissingEqualsError +import org.oppia.android.util.math.PeekableIterator.Companion.toPeekableIterator class MathExpressionParser private constructor(private val parseContext: ParseContext) { // TODO: @@ -637,7 +638,7 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo private sealed class ParseContext(val rawExpression: String) { val tokens: PeekableIterator by lazy { - PeekableIterator.fromSequence(MathTokenizer.tokenize(rawExpression)) + MathTokenizer.tokenize(rawExpression).toPeekableIterator() } private var previousToken: Token? = null From f5b657175b33b88148a9c333692b60ee43d101a6 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 21 Jan 2022 17:56:33 -0800 Subject: [PATCH 052/134] Add error tests. These tests are more or less comprehensive based on existing tests and some new ideas. All errors are now covered by MathExpressionParserTest. Error ordering is not tested. A new Truth subject was added for easier testing, as well (for MathParsingError). --- .../oppia/android/testing/math/BUILD.bazel | 17 + .../testing/math/MathParsingErrorSubject.kt | 268 ++++ .../android/util/math/MathExpressionParser.kt | 115 +- .../util/math/AlgebraicEquationParserTest.kt | 12 - .../math/AlgebraicExpressionParserTest.kt | 54 +- .../org/oppia/android/util/math/BUILD.bazel | 4 +- .../util/math/MathExpressionParserTest.kt | 1278 +++++++++++++---- .../util/math/NumericExpressionParserTest.kt | 64 +- 8 files changed, 1411 insertions(+), 401 deletions(-) create mode 100644 testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt diff --git a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel index ea0b8ba6b54..d60f0f8aafa 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel @@ -74,6 +74,23 @@ kt_android_library( ], ) +kt_android_library( + name = "math_parsing_error_subject", + testonly = True, + srcs = [ + "MathParsingErrorSubject.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":math_expression_subject", + ":real_subject", + "//third_party:com_google_truth_truth", + "//utility/src/main/java/org/oppia/android/util/math:parsing_error", + ], +) + kt_android_library( name = "polynomial_subject", testonly = True, diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt new file mode 100644 index 00000000000..5db1b5f61d9 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt @@ -0,0 +1,268 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.ComparableSubject +import com.google.common.truth.FailureMetadata +import com.google.common.truth.IterableSubject +import com.google.common.truth.StringSubject +import com.google.common.truth.Subject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat +import org.oppia.android.testing.math.RealSubject.Companion.assertThat +import org.oppia.android.util.math.MathParsingError +import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError +import org.oppia.android.util.math.MathParsingError.EquationHasTooManyEqualsError +import org.oppia.android.util.math.MathParsingError.EquationIsMissingEqualsError +import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError +import org.oppia.android.util.math.MathParsingError.ExponentIsVariableExpressionError +import org.oppia.android.util.math.MathParsingError.ExponentTooLargeError +import org.oppia.android.util.math.MathParsingError.FunctionNameIncompleteError +import org.oppia.android.util.math.MathParsingError.GenericError +import org.oppia.android.util.math.MathParsingError.HangingSquareRootError +import org.oppia.android.util.math.MathParsingError.InvalidFunctionInUseError +import org.oppia.android.util.math.MathParsingError.MultipleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.NestedExponentsError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberAfterBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberBeforeBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NumberAfterVariableError +import org.oppia.android.util.math.MathParsingError.RedundantParenthesesForIndividualTermsError +import org.oppia.android.util.math.MathParsingError.SingleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.SpacesBetweenNumbersError +import org.oppia.android.util.math.MathParsingError.SubsequentBinaryOperatorsError +import org.oppia.android.util.math.MathParsingError.SubsequentUnaryOperatorsError +import org.oppia.android.util.math.MathParsingError.TermDividedByZeroError +import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError +import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError +import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError + +// TODO: file issue to add tests. + +class MathParsingErrorSubject( + metadata: FailureMetadata, + private val actual: MathParsingError +) : Subject(metadata, actual) { + fun isSpacesBetweenNumbers() { + assertThat(actual).isEqualTo(SpacesBetweenNumbersError) + } + + fun isUnbalancedParentheses() { + assertThat(actual).isEqualTo(UnbalancedParenthesesError) + } + + fun isSingleRedundantParenthesesThat(): SingleRedundantParenthesesSubject { + return SingleRedundantParenthesesSubject.assertThat(verifyAsType()) + } + + fun isMultipleRedundantParenthesesThat(): MultipleRedundantParenthesesSubject { + return MultipleRedundantParenthesesSubject.assertThat(verifyAsType()) + } + + fun isRedundantIndividualTermsParensThat(): RedundantParenthesesForIndividualTermsSubject { + return RedundantParenthesesForIndividualTermsSubject.assertThat(verifyAsType()) + } + + fun isUnnecessarySymbolWithSymbolThat(): StringSubject { + return assertThat(verifyAsType().invalidSymbol) + } + + fun isNumberAfterVariableThat(): NumberAfterVariableSubject { + return NumberAfterVariableSubject.assertThat(verifyAsType()) + } + + fun isSubsequentBinaryOperatorsThat(): SubsequentBinaryOperatorsSubject { + return SubsequentBinaryOperatorsSubject.assertThat(verifyAsType()) + } + + fun isSubsequentUnaryOperators() { + assertThat(actual).isEqualTo(SubsequentUnaryOperatorsError) + } + + fun isNoVarOrNumBeforeBinaryOperatorThat(): NoVariableOrNumberBeforeBinaryOperatorSubject { + return NoVariableOrNumberBeforeBinaryOperatorSubject.assertThat(verifyAsType()) + } + + fun isNoVariableOrNumberAfterBinaryOperatorThat(): NoVariableOrNumberAfterBinaryOperatorSubject { + return NoVariableOrNumberAfterBinaryOperatorSubject.assertThat(verifyAsType()) + } + + fun isExponentIsVariableExpression() { + assertThat(actual).isEqualTo(ExponentIsVariableExpressionError) + } + + fun isExponentTooLarge() { + assertThat(actual).isEqualTo(ExponentTooLargeError) + } + + fun isNestedExponents() { + assertThat(actual).isEqualTo(NestedExponentsError) + } + + fun isHangingSquareRoot() { + assertThat(actual).isEqualTo(HangingSquareRootError) + } + + fun isTermDividedByZero() { + assertThat(actual).isEqualTo(TermDividedByZeroError) + } + + fun isVariableInNumericExpression() { + assertThat(actual).isEqualTo(VariableInNumericExpressionError) + } + + fun isDisabledVariablesInUseWithVariablesThat(): IterableSubject { + return assertThat(verifyAsType().variables) + } + + fun isEquationIsMissingEquals() { + assertThat(actual).isEqualTo(EquationIsMissingEqualsError) + } + + fun isEquationHasTooManyEquals() { + assertThat(actual).isEqualTo(EquationHasTooManyEqualsError) + } + + fun isEquationMissingLhsOrRhs() { + assertThat(actual).isEqualTo(EquationMissingLhsOrRhsError) + } + + fun isInvalidFunctionInUseWithNameThat(): StringSubject { + return assertThat(verifyAsType().functionName) + } + + fun isFunctionNameIncomplete() { + assertThat(actual).isEqualTo(FunctionNameIncompleteError) + } + + fun isGenericError() { + assertThat(actual).isEqualTo(GenericError) + } + + private inline fun verifyAsType(): T { + assertThat(actual).isInstanceOf(T::class.java) + return actual as T + } + + class SingleRedundantParenthesesSubject( + metadata: FailureMetadata, + private val actual: SingleRedundantParenthesesError + ) : Subject(metadata, actual) { + fun hasRawExpressionThat(): StringSubject = assertThat(actual.rawExpression) + + fun hasExpressionThat(): MathExpressionSubject = assertThat(actual.expression) + + companion object { + internal fun assertThat( + actual: SingleRedundantParenthesesError + ): SingleRedundantParenthesesSubject { + return assertAbout(::SingleRedundantParenthesesSubject).that(actual) + } + } + } + + class MultipleRedundantParenthesesSubject( + metadata: FailureMetadata, + private val actual: MultipleRedundantParenthesesError + ) : Subject(metadata, actual) { + fun hasRawExpressionThat(): StringSubject = assertThat(actual.rawExpression) + + fun hasExpressionThat(): MathExpressionSubject = assertThat(actual.expression) + + companion object { + internal fun assertThat( + actual: MultipleRedundantParenthesesError + ): MultipleRedundantParenthesesSubject { + return assertAbout(::MultipleRedundantParenthesesSubject).that(actual) + } + } + } + + class RedundantParenthesesForIndividualTermsSubject( + metadata: FailureMetadata, + private val actual: RedundantParenthesesForIndividualTermsError + ) : Subject(metadata, actual) { + fun hasRawExpressionThat(): StringSubject = assertThat(actual.rawExpression) + + fun hasExpressionThat(): MathExpressionSubject = assertThat(actual.expression) + + companion object { + internal fun assertThat( + actual: RedundantParenthesesForIndividualTermsError + ): RedundantParenthesesForIndividualTermsSubject { + return assertAbout(::RedundantParenthesesForIndividualTermsSubject).that(actual) + } + } + } + + class NumberAfterVariableSubject( + metadata: FailureMetadata, + private val actual: NumberAfterVariableError + ) : Subject(metadata, actual) { + fun hasNumberThat(): RealSubject = assertThat(actual.number) + + fun hasVariableThat(): StringSubject = assertThat(actual.variable) + + companion object { + internal fun assertThat(actual: NumberAfterVariableError): NumberAfterVariableSubject = + assertAbout(::NumberAfterVariableSubject).that(actual) + } + } + + class SubsequentBinaryOperatorsSubject( + metadata: FailureMetadata, + private val actual: SubsequentBinaryOperatorsError + ) : Subject(metadata, actual) { + fun hasFirstOperatorThat(): StringSubject = assertThat(actual.operator1) + + fun hasSecondOperatorThat(): StringSubject = assertThat(actual.operator2) + + companion object { + internal fun assertThat( + actual: SubsequentBinaryOperatorsError + ): SubsequentBinaryOperatorsSubject { + return assertAbout(::SubsequentBinaryOperatorsSubject).that(actual) + } + } + } + + class NoVariableOrNumberBeforeBinaryOperatorSubject( + metadata: FailureMetadata, + private val actual: NoVariableOrNumberBeforeBinaryOperatorError + ) : Subject(metadata, actual) { + fun hasOperatorThat(): ComparableSubject = + assertThat(actual.operator) + + fun hasOperatorSymbolThat(): StringSubject = assertThat(actual.operatorSymbol) + + companion object { + internal fun assertThat( + actual: NoVariableOrNumberBeforeBinaryOperatorError + ): NoVariableOrNumberBeforeBinaryOperatorSubject { + return assertAbout(::NoVariableOrNumberBeforeBinaryOperatorSubject).that(actual) + } + } + } + + class NoVariableOrNumberAfterBinaryOperatorSubject( + metadata: FailureMetadata, + private val actual: NoVariableOrNumberAfterBinaryOperatorError + ) : Subject(metadata, actual) { + fun hasOperatorThat(): ComparableSubject = + assertThat(actual.operator) + + fun hasOperatorSymbolThat(): StringSubject = assertThat(actual.operatorSymbol) + + companion object { + internal fun assertThat( + actual: NoVariableOrNumberAfterBinaryOperatorError + ): NoVariableOrNumberAfterBinaryOperatorSubject { + return assertAbout(::NoVariableOrNumberAfterBinaryOperatorSubject).that(actual) + } + } + } + + companion object { + fun assertThat(actual: MathParsingError): MathParsingErrorSubject = + assertAbout(::MathParsingErrorSubject).that(actual) + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt index 688ba4408fd..e0afccf30e4 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -62,6 +62,7 @@ import org.oppia.android.util.math.MathTokenizer.Companion.Token.RightParenthesi import org.oppia.android.util.math.MathTokenizer.Companion.Token.SquareRootSymbol import org.oppia.android.util.math.MathTokenizer.Companion.Token.VariableName import kotlin.math.absoluteValue +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator import org.oppia.android.util.math.MathParsingError.EquationHasTooManyEqualsError import org.oppia.android.util.math.MathParsingError.EquationIsMissingEqualsError import org.oppia.android.util.math.PeekableIterator.Companion.toPeekableIterator @@ -565,40 +566,82 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } private fun checkForLearnerErrors(expression: MathExpression): MathParsingError? { - val firstMultiRedundantGroup = expression.findFirstMultiRedundantGroup() - val nextRedundantGroup = expression.findNextRedundantGroup() - val nextUnaryOperation = expression.findNextRedundantUnaryOperation() - val nextExpWithVariableExp = expression.findNextExponentiationWithVariablePower() - val nextExpWithTooLargePower = expression.findNextExponentiationWithTooLargePower() - val nextExpWithNestedExp = expression.findNextNestedExponentiation() - val nextDivByZero = expression.findNextDivisionByZero() - val disallowedVariables = expression.findAllDisallowedVariables(parseContext) // Note that the order of checks here is important since errors have precedence, and some are // redundant and, in the wrong order, may cause the wrong error to be returned. val includeOptionalErrors = parseContext.errorCheckingMode.includesOptionalErrors() - return when { - includeOptionalErrors && firstMultiRedundantGroup != null -> { - val subExpression = parseContext.extractSubexpression(firstMultiRedundantGroup) - MultipleRedundantParenthesesError(subExpression, firstMultiRedundantGroup) - } - includeOptionalErrors && expression.expressionTypeCase == GROUP -> - SingleRedundantParenthesesError(parseContext.rawExpression, expression) - includeOptionalErrors && nextRedundantGroup != null -> { - val subExpression = parseContext.extractSubexpression(nextRedundantGroup) - RedundantParenthesesForIndividualTermsError(subExpression, nextRedundantGroup) - } - includeOptionalErrors && nextUnaryOperation != null -> SubsequentUnaryOperatorsError - includeOptionalErrors && nextExpWithVariableExp != null -> ExponentIsVariableExpressionError - includeOptionalErrors && nextExpWithTooLargePower != null -> ExponentTooLargeError - includeOptionalErrors && nextExpWithNestedExp != null -> NestedExponentsError - includeOptionalErrors && nextDivByZero != null -> TermDividedByZeroError - includeOptionalErrors && disallowedVariables.isNotEmpty() -> - DisabledVariablesInUseError(disallowedVariables.toList()) - else -> ensureNoRemainingTokens() + val optionalError = if (includeOptionalErrors) { + checkForFirstRedundantGroupError(expression) + ?: checkForWholeExpressionGroupRedundancy(expression) + ?: checkForRedundantGroupError(expression) + ?: checkForRedundantUnaryOperation(expression) + ?: checkForExponentVariablePowers(expression) + ?: checkForTooLargeExponentPower(expression) + ?: checkForNestedExponentiations(expression) + ?: checkForDivisionByZero(expression) + ?: checkForDisallowedVariables(expression) + ?: checkForUnaryPlus(expression) + } else null + return optionalError ?: checkForRemainingTokens() + } + + private fun checkForFirstRedundantGroupError(expression: MathExpression): MathParsingError? { + return expression.findFirstMultiRedundantGroup()?.let { firstMultiRedundantGroup -> + val subExpression = parseContext.extractSubexpression(firstMultiRedundantGroup) + MultipleRedundantParenthesesError(subExpression, firstMultiRedundantGroup) + } + } + + private fun checkForWholeExpressionGroupRedundancy( + expression: MathExpression + ): MathParsingError? { + return if (expression.expressionTypeCase == GROUP) { + SingleRedundantParenthesesError(parseContext.extractSubexpression(expression), expression) + } else null + } + + private fun checkForRedundantGroupError(expression: MathExpression): MathParsingError? { + return expression.findNextRedundantGroup()?.let { nextRedundantGroup -> + val subExpression = parseContext.extractSubexpression(nextRedundantGroup) + RedundantParenthesesForIndividualTermsError(subExpression, nextRedundantGroup) + } + } + + private fun checkForRedundantUnaryOperation(expression: MathExpression): MathParsingError? { + return expression.findNextRedundantUnaryOperation()?.let { SubsequentUnaryOperatorsError } + } + + private fun checkForExponentVariablePowers(expression: MathExpression): MathParsingError? { + return expression.findNextExponentiationWithVariablePower()?.let { + ExponentIsVariableExpressionError + } + } + + private fun checkForTooLargeExponentPower(expression: MathExpression): MathParsingError? { + return expression.findNextExponentiationWithTooLargePower()?.let { ExponentTooLargeError } + } + + private fun checkForNestedExponentiations(expression: MathExpression): MathParsingError? { + return expression.findNextNestedExponentiation()?.let { NestedExponentsError } + } + + private fun checkForDivisionByZero(expression: MathExpression): MathParsingError? { + return expression.findNextDivisionByZero()?.let { TermDividedByZeroError } + } + + private fun checkForDisallowedVariables(expression: MathExpression): MathParsingError? { + return expression.findAllDisallowedVariables(parseContext).takeIf { it.isNotEmpty() }?.let { + DisabledVariablesInUseError(it.toList()) } } - private fun ensureNoRemainingTokens(): MathParsingError? { + private fun checkForUnaryPlus(expression: MathExpression): MathParsingError? { + return expression.findNextUnaryPlus()?.let { + // The operatorSymbol can't be trivially extracted, so just force it to '+' for correctness. + NoVariableOrNumberBeforeBinaryOperatorError(ADD, operatorSymbol = "+") + } + } + + private fun checkForRemainingTokens(): MathParsingError? { // Make sure all tokens were consumed (otherwise there are trailing tokens which invalidate the // whole grammar). return if (parseContext.hasMoreTokens()) { @@ -1030,6 +1073,22 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } } + private fun MathExpression.findNextUnaryPlus(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> + binaryOperation.leftOperand.findNextUnaryPlus() + ?: binaryOperation.rightOperand.findNextUnaryPlus() + UNARY_OPERATION -> when (unaryOperation.operator) { + POSITIVE -> this + NEGATE, UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> + unaryOperation.operand.findNextUnaryPlus() + } + FUNCTION_CALL -> functionCall.argument.findNextUnaryPlus() + GROUP -> group.findNextUnaryPlus() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + private fun MathExpression.isVariableExpression(): Boolean { return when (expressionTypeCase) { VARIABLE -> true diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt index 94fc6e50ab4..8e94d04c379 100644 --- a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt @@ -16,9 +16,6 @@ import org.robolectric.annotation.LooperMode class AlgebraicEquationParserTest { @Test fun testLotsOfCasesForAlgebraicEquation() { - expectFailureWhenParsingAlgebraicEquation(" x =") - expectFailureWhenParsingAlgebraicEquation(" = y") - val equation1 = parseAlgebraicEquationSuccessfully("x = 1") assertThat(equation1).hasLeftHandSideThat().hasStructureThatMatches { variable { @@ -134,9 +131,6 @@ class AlgebraicEquationParserTest { } } - expectFailureWhenParsingAlgebraicEquation("y = (x+1)(x-1) 2") - expectFailureWhenParsingAlgebraicEquation("y 2 = (x+1)(x-1)") - val equation5 = parseAlgebraicEquationSuccessfully( "a*x^2 + b*x + c = 0", allowedVariables = listOf("x", "a", "b", "c") @@ -201,12 +195,6 @@ class AlgebraicEquationParserTest { private companion object { // TODO: fix helper API. - private fun expectFailureWhenParsingAlgebraicEquation(expression: String): MathParsingError { - val result = parseAlgebraicEquationWithAllErrors(expression) - assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) - return (result as MathParsingResult.Failure).error - } - private fun parseAlgebraicEquationSuccessfully( expression: String, allowedVariables: List = listOf("x", "y", "z") diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt index 9fea4084970..e05d9c5906f 100644 --- a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt @@ -19,8 +19,6 @@ class AlgebraicExpressionParserTest { fun testLotsOfCasesForAlgebraicExpression() { // TODO: split this up // TODO: add log string generation for expressions. - expectFailureWhenParsingAlgebraicExpression("") - val expression1 = parseAlgebraicExpressionWithAllErrors("1") assertThat(expression1).hasStructureThatMatches { constant { @@ -187,8 +185,6 @@ class AlgebraicExpressionParserTest { } } - expectFailureWhenParsingAlgebraicExpression("sqr(2)") - val expression64 = parseAlgebraicExpressionWithoutOptionalErrors("xyz(2)") assertThat(expression64).hasStructureThatMatches { multiplication { @@ -232,8 +228,6 @@ class AlgebraicExpressionParserTest { } } - expectFailureWhenParsingAlgebraicExpression("73 2") - // Verify order of operations between higher & lower precedent operators. val expression32 = parseAlgebraicExpressionWithAllErrors("3+4^5") assertThat(expression32).hasStructureThatMatches { @@ -357,8 +351,6 @@ class AlgebraicExpressionParserTest { } } - expectFailureWhenParsingAlgebraicExpression("x = √2 × 7 ÷ 4") - val expression8 = parseAlgebraicExpressionWithAllErrors("(1+2)(3+4)") assertThat(expression8).hasStructureThatMatches { multiplication { @@ -397,9 +389,6 @@ class AlgebraicExpressionParserTest { } } - // Right implicit multiplication of numbers isn't allowed. - expectFailureWhenParsingAlgebraicExpression("(1+2)2") - val expression10 = parseAlgebraicExpressionWithAllErrors("2(1+2)") assertThat(expression10).hasStructureThatMatches { multiplication { @@ -427,9 +416,6 @@ class AlgebraicExpressionParserTest { } } - // Right implicit multiplication of numbers isn't allowed. - expectFailureWhenParsingAlgebraicExpression("sqrt(2)3") - val expression12 = parseAlgebraicExpressionWithAllErrors("3sqrt(2)") assertThat(expression12).hasStructureThatMatches { multiplication { @@ -651,7 +637,7 @@ class AlgebraicExpressionParserTest { } } - val expression18 = parseAlgebraicExpressionWithAllErrors("1++4") + val expression18 = parseAlgebraicExpressionWithoutOptionalErrors("1++4") assertThat(expression18).hasStructureThatMatches { addition { leftOperand { @@ -691,8 +677,6 @@ class AlgebraicExpressionParserTest { } } - expectFailureWhenParsingAlgebraicExpression("1-^-4") - val expression20 = parseAlgebraicExpressionWithAllErrors("√2 × 7 ÷ 4") assertThat(expression20).hasStructureThatMatches { division { @@ -722,8 +706,6 @@ class AlgebraicExpressionParserTest { } } - expectFailureWhenParsingAlgebraicExpression("1+2 &asdf") - val expression21 = parseAlgebraicExpressionWithAllErrors("sqrt(2)sqrt(3)sqrt(4)") // Note that this tree demonstrates left associativity. assertThat(expression21).hasStructureThatMatches { @@ -1005,13 +987,6 @@ class AlgebraicExpressionParserTest { } } - // Numbers cannot have implicit multiplication unless they are in groups. - expectFailureWhenParsingAlgebraicExpression("2 2") - - expectFailureWhenParsingAlgebraicExpression("2 2^2") - - expectFailureWhenParsingAlgebraicExpression("2^2 2") - val expression31 = parseAlgebraicExpressionWithoutOptionalErrors("(3)(4)(5)") assertThat(expression31).hasStructureThatMatches { multiplication { @@ -1091,9 +1066,6 @@ class AlgebraicExpressionParserTest { } } - // An exponentiation can never be an implicit right operand. - expectFailureWhenParsingAlgebraicExpression("2^(3)2^2") - val expression35 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)*2^2") assertThat(expression35).hasStructureThatMatches { multiplication { @@ -1169,9 +1141,6 @@ class AlgebraicExpressionParserTest { } } - // An exponentiation can never be an implicit right operand. - expectFailureWhenParsingAlgebraicExpression("2^3(4)2^3") - val expression38 = parseAlgebraicExpressionWithoutOptionalErrors("2^3(4)*2^3") assertThat(expression38).hasStructureThatMatches { // 2^3(4)*2^3 @@ -1226,14 +1195,6 @@ class AlgebraicExpressionParserTest { } } - expectFailureWhenParsingAlgebraicExpression("2^2 2^2") - expectFailureWhenParsingAlgebraicExpression("(3) 2^2") - expectFailureWhenParsingAlgebraicExpression("sqrt(3) 2^2") - expectFailureWhenParsingAlgebraicExpression("√2 2^2") - expectFailureWhenParsingAlgebraicExpression("2^2 3") - - expectFailureWhenParsingAlgebraicExpression("-2 3") - val expression39 = parseAlgebraicExpressionWithAllErrors("-(1+2)") assertThat(expression39).hasStructureThatMatches { negation { @@ -1684,9 +1645,6 @@ class AlgebraicExpressionParserTest { } } - // Should fail for algebra. - expectFailureWhenParsingAlgebraicExpression("x7") - // Should pass for algebra. val expression67 = parseAlgebraicExpressionWithAllErrors("2x^2y^-3") assertThat(expression67).hasStructureThatMatches { @@ -1894,16 +1852,6 @@ class AlgebraicExpressionParserTest { private companion object { // TODO: fix helper API. - private fun expectFailureWhenParsingAlgebraicExpression( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathParsingError { - val result = - parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) - assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) - return (result as MathParsingResult.Failure).error - } - private fun parseAlgebraicExpressionWithoutOptionalErrors( expression: String, allowedVariables: List = listOf("x", "y", "z") 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 index 02ed22ac702..5d2a0422271 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -86,7 +86,9 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", - "//third_party:androidx_test_ext_junit", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/math:math_parsing_error_subject", "//third_party:com_google_truth_extensions_truth-liteproto-extension", "//third_party:com_google_truth_truth", "//third_party:junit_junit", diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt index 88e0a46ac79..f74e1db819a 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -1,348 +1,1138 @@ package org.oppia.android.util.math -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.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.math.MathParsingErrorSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.REQUIRED_ONLY import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult -import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError -import org.oppia.android.util.math.MathParsingError.EquationHasTooManyEqualsError -import org.oppia.android.util.math.MathParsingError.EquationIsMissingEqualsError -import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError -import org.oppia.android.util.math.MathParsingError.ExponentIsVariableExpressionError -import org.oppia.android.util.math.MathParsingError.ExponentTooLargeError -import org.oppia.android.util.math.MathParsingError.FunctionNameIncompleteError -import org.oppia.android.util.math.MathParsingError.HangingSquareRootError -import org.oppia.android.util.math.MathParsingError.InvalidFunctionInUseError -import org.oppia.android.util.math.MathParsingError.MultipleRedundantParenthesesError -import org.oppia.android.util.math.MathParsingError.NestedExponentsError -import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberAfterBinaryOperatorError -import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberBeforeBinaryOperatorError -import org.oppia.android.util.math.MathParsingError.NumberAfterVariableError -import org.oppia.android.util.math.MathParsingError.RedundantParenthesesForIndividualTermsError -import org.oppia.android.util.math.MathParsingError.SingleRedundantParenthesesError -import org.oppia.android.util.math.MathParsingError.SpacesBetweenNumbersError -import org.oppia.android.util.math.MathParsingError.SubsequentBinaryOperatorsError -import org.oppia.android.util.math.MathParsingError.SubsequentUnaryOperatorsError -import org.oppia.android.util.math.MathParsingError.TermDividedByZeroError -import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError -import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError -import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError import org.robolectric.annotation.LooperMode -/** Tests for [MathExpressionParser]. */ -@RunWith(AndroidJUnit4::class) +/** + * Tests for [MathExpressionParser]. + * + * Note that while this verifies that, at a high level, numeric expressions, algebraic expressions, + * and algebraic equations can be successfully parsed, it mainly focuses on errors (and some passing + * cases that closely relate to possible errors). + * + * Further, this mainly relies on numeric expressions to ensure error detection works since it's + * assumed that algebraic equations rely on algebraic expressions, and algebraic expressions rely on + * numeric expressions. + * + * Finally, there are dedicated test suites for each of numeric expressions + * [NumericExpressionParserTest], algebraic expressions [AlgebraicExpressionParserTest], and + * algebraic equations [AlgebraicEquationParserTest]. + */ +// FunctionName: test names are conventionally named with underscores. +// SameParameterValue: tests should have specific context included/excluded for readability. +@Suppress("FunctionName", "SameParameterValue") +@RunWith(OppiaParameterizedTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class MathExpressionParserTest { - // TODO: add high-level checks for the three types, but don't test in detail since there are - // separate suites. Also, document the separate suites' existence in this suites's KDoc. + @Parameter + lateinit var lhsOp: String + @Parameter + lateinit var rhsOp: String + @Parameter + lateinit var binOp: String + @Parameter + lateinit var subExp: String + @Parameter + lateinit var func: String @Test - fun testErrorCases() { - // TODO: split up. - val failure1 = expectFailureWhenParsingNumericExpression("73 2") - assertThat(failure1).isEqualTo(SpacesBetweenNumbersError) + fun testParseNumExp_basicExpression_doesNotFail() { + expectSuccessWhenParsingNumericExpression("1 + 2") + } - val failure2 = expectFailureWhenParsingNumericExpression("(73") - assertThat(failure2).isEqualTo(UnbalancedParenthesesError) + @Test + fun testParseAlgExp_basicExpression_doesNotFail() { + expectSuccessWhenParsingAlgebraicExpression("x + y") + } - val failure3 = expectFailureWhenParsingNumericExpression("73)") - assertThat(failure3).isEqualTo(UnbalancedParenthesesError) + @Test + fun testParseAlgebraicEquation_basicEquation_doesNotFail() { + expectSuccessWhenParsingAlgebraicEquation("y = 2x + 3") + } - val failure4 = expectFailureWhenParsingNumericExpression("((73)") - assertThat(failure4).isEqualTo(UnbalancedParenthesesError) + @Test + fun testParseNumExp_emptyExpression_returnsGenericError() { + val error = expectFailureWhenParsingNumericExpression("") - val failure5 = expectFailureWhenParsingNumericExpression("73 (") - assertThat(failure5).isEqualTo(UnbalancedParenthesesError) + assertThat(error).isGenericError() + } - val failure6 = expectFailureWhenParsingNumericExpression("73 )") - assertThat(failure6).isEqualTo(UnbalancedParenthesesError) + @Test + fun testParseNumExp_numbersWithSpaces_returnsSpacesBetweenNumbersError() { + val error = expectFailureWhenParsingNumericExpression("73 2") - val failure7 = expectFailureWhenParsingNumericExpression("sqrt(73") - assertThat(failure7).isEqualTo(UnbalancedParenthesesError) + // Numbers cannot be implicitly multiplied unless they are in groups. + assertThat(error).isSpacesBetweenNumbers() + } - // TODO: test properties on errors (& add better testing library for errors, or at least helpers). - val failure8 = expectFailureWhenParsingNumericExpression("(7 * 2 + 4)") - assertThat(failure8).isInstanceOf(SingleRedundantParenthesesError::class.java) + @Test + fun testParseNumExp_spaceBetweenNegatedAndRegularNumber_returnsSpacesBetweenNumbersError() { + val error = expectFailureWhenParsingNumericExpression("-2 3") - val failure9 = expectFailureWhenParsingNumericExpression("((5 + 4))") - assertThat(failure9).isInstanceOf(MultipleRedundantParenthesesError::class.java) + assertThat(error).isSpacesBetweenNumbers() + } - val failure13 = expectFailureWhenParsingNumericExpression("(((5 + 4)))") - assertThat(failure13).isInstanceOf(MultipleRedundantParenthesesError::class.java) + @Test + fun testParseNumExp_numberAndExponentSeparatedBySpace_returnsSpacesBetweenNumbersError() { + val error = expectFailureWhenParsingNumericExpression("2 2^2") - val failure14 = expectFailureWhenParsingNumericExpression("1+((5 + 4))") - assertThat(failure14).isInstanceOf(MultipleRedundantParenthesesError::class.java) + // Unless a similar algebraic expression (e.g. 2x^2), this is not valid. + assertThat(error).isSpacesBetweenNumbers() + } - val failure15 = expectFailureWhenParsingNumericExpression("1+(7*((( 9 + 3) )))") - assertThat(failure15).isInstanceOf(MultipleRedundantParenthesesError::class.java) - assertThat((failure15 as MultipleRedundantParenthesesError).rawExpression) - .isEqualTo("(( 9 + 3) )") + @Test + fun testParseNumExp_squareRootAndExponentSeparatedBySpace_returnsSpacesBetweenNumbersError() { + val error = expectFailureWhenParsingNumericExpression("√2 2^2") - parseNumericExpressionSuccessfully("1+(5+4)") - parseNumericExpressionSuccessfully("(5+4)+1") + // Ensure the square root special case doesn't change the implicit multiplication rule for + // numbers. + assertThat(error).isSpacesBetweenNumbers() + } - val failure10 = expectFailureWhenParsingNumericExpression("(5) + 4") - assertThat(failure10).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) + @Test + fun testParseNumExp_exponentAndNumberSeparatedBySpace_returnsSpacesBetweenNumbersError() { + val error = expectFailureWhenParsingNumericExpression("2^2 2") - val failure11 = expectFailureWhenParsingNumericExpression("5^(2)") - assertThat(failure11).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) - assertThat((failure11 as RedundantParenthesesForIndividualTermsError).rawExpression) - .isEqualTo("2") + // Right implicit multiplication for numbers is never allowed. + assertThat(error).isSpacesBetweenNumbers() + } - val failure12 = expectFailureWhenParsingNumericExpression("sqrt((2))") - assertThat(failure12).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) + @Test + fun testParseNumExp_twoExponentsSeparatedBySpace_returnsSpacesBetweenNumbersError() { + val error = expectFailureWhenParsingNumericExpression("2^2 3^2") - val failure16 = expectFailureWhenParsingNumericExpression("$2") - assertThat(failure16).isInstanceOf(UnnecessarySymbolsError::class.java) - assertThat((failure16 as UnnecessarySymbolsError).invalidSymbol).isEqualTo("$") + // Subsequent exponents are never implicitly multiplied for numeric expressions. + assertThat(error).isSpacesBetweenNumbers() + } - val failure17 = expectFailureWhenParsingNumericExpression("5%") - assertThat(failure17).isInstanceOf(UnnecessarySymbolsError::class.java) - assertThat((failure17 as UnnecessarySymbolsError).invalidSymbol).isEqualTo("%") + @Test + fun testParseAlgExp_numberAndVariableBaseExponentSeparatedBySpace_doesNotFail() { + // Implicit multiplication with numbers on the left is allowed if the right is a variable raised + // to a power (in order to support polynomial syntax). + expectSuccessWhenParsingAlgebraicExpression("2 x^2") + } - val failure18 = expectFailureWhenParsingAlgebraicExpression("x5") - assertThat(failure18).isInstanceOf(NumberAfterVariableError::class.java) - assertThat((failure18 as NumberAfterVariableError).number.integer).isEqualTo(5) - assertThat(failure18.variable).isEqualTo("x") + @Test + fun testParseAlgExp_varBaseExponentAndNumberSeparatedBySpace_returnsSpacesBetweenNumbersError() { + val error = expectFailureWhenParsingAlgebraicExpression("x^2 2") + + // Right implicit multiplication for numbers is never allowed. + assertThat(error).isSpacesBetweenNumbers() + } + + @Test + fun testParseAlgExp_twoAdjacentVariableBaseExponentsSeparatedBySpace_doesNotFail() { + // Similarly, this is supported for polynomial syntax. + expectSuccessWhenParsingAlgebraicExpression("x^2 y^2") + } + + @Test + fun testParseAlgExp_twoAdjacentNumericExponentsSepBySpace_returnsSpacesBetweenNumbersError() { + val error = expectFailureWhenParsingAlgebraicExpression("2^2 3^2") + + // While the variable version of this works, the numeric does not (explicit multiplication is + // required). + assertThat(error).isSpacesBetweenNumbers() + } - val failure19 = expectFailureWhenParsingAlgebraicExpression("2+y 3.14*7") - assertThat(failure19).isInstanceOf(NumberAfterVariableError::class.java) - assertThat((failure19 as NumberAfterVariableError).number.irrational).isWithin(1e-5).of(3.14) - assertThat(failure19.variable).isEqualTo("y") + @Test + fun testParseNumExp_leftParenAndNumber_returnsUnbalancedParenthesesError() { + val error = expectFailureWhenParsingNumericExpression("(73") + + assertThat(error).isUnbalancedParentheses() + } + + @Test + fun testParseNumExp_numberAndRightParen_returnsUnbalancedParenthesesError() { + val error = expectFailureWhenParsingNumericExpression("73)") - // TODO: expand to multiple tests or use parametrized tests. - // RHS operators don't result in unary operations (which are valid in the grammar). - val rhsOperators = listOf("*", "×", "/", "÷", "^") - val lhsOperators = rhsOperators + listOf("+", "-", "−") - val operatorCombinations = lhsOperators.flatMap { op1 -> rhsOperators.map { op1 to it } } - for ((op1, op2) in operatorCombinations) { - val failure22 = expectFailureWhenParsingNumericExpression(expression = "1 $op1$op2 2") - assertThat(failure22).isInstanceOf(SubsequentBinaryOperatorsError::class.java) - assertThat((failure22 as SubsequentBinaryOperatorsError).operator1).isEqualTo(op1) - assertThat(failure22.operator2).isEqualTo(op2) + assertThat(error).isUnbalancedParentheses() + } + + @Test + fun testParseNumExp_numberGroup_extraLeftParen_returnsUnbalancedParenthesesError() { + val error = expectFailureWhenParsingNumericExpression("((73)") + + assertThat(error).isUnbalancedParentheses() + } + + @Test + fun testParseNumExp_number_hangingLeftParen_returnsUnbalancedParenthesesError() { + val error = expectFailureWhenParsingNumericExpression("73 (") + + assertThat(error).isUnbalancedParentheses() + } + + @Test + fun testParseNumExp_number_hangingRightParen_returnsUnbalancedParenthesesError() { + val error = expectFailureWhenParsingNumericExpression("73 )") + + assertThat(error).isUnbalancedParentheses() + } + + @Test + fun testParseNumExp_sqrt_missingRightParen_returnsUnbalancedParenthesesError() { + val error = expectFailureWhenParsingNumericExpression("sqrt(73") + + assertThat(error).isUnbalancedParentheses() + } + + @Test + fun testParseNumExp_outerExpressionGrouped_returnsRedundantParensErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("(7 * 2 + 4)") + + assertThat(error).isSingleRedundantParenthesesThat().apply { + // The valid sub-expression should be captured as part of the error. + hasRawExpressionThat().isEqualTo("(7 * 2 + 4)") + hasExpressionThat().hasStructureThatMatches { + group { + addition { + leftOperand { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + @Test + fun testParseNumExp_leftExpInRightNumericImplicitMult_returnsRedundantParensErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("(1 + 2)2") + + // Note that this error occurs because the '2' is considered an extra token in the token stream, + // so only '(1 + 2)' is parsed. + assertThat(error).isSingleRedundantParenthesesThat().apply { + // The valid sub-expression should be captured as part of the error. + hasRawExpressionThat().isEqualTo("(1 + 2)") + hasExpressionThat().hasStructureThatMatches { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + @Test + fun testParseNumExp_rightExpInLeftNumericImplicitMult_doesNotFail() { + // As compared with the above, implicit left multiplication is supported with numbers when + // parentheses are used. + expectSuccessWhenParsingNumericExpression("2(1 + 2)") + } + + @Test + fun testParseNumExp_singleVarTermInImplicitExpMult_returnsRedundantParensErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("(3) 2^2") + + // Note that this error occurs because the '2^2' is considered extra tokens in the token stream, + // so only '(3)' is parsed. + assertThat(error).isSingleRedundantParenthesesThat().apply { + // The valid sub-expression should be captured as part of the error. + hasRawExpressionThat().isEqualTo("(3)") + hasExpressionThat().hasStructureThatMatches { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } } + } - val failure37 = expectFailureWhenParsingNumericExpression("++2") - assertThat(failure37).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + @Test + fun testParseNumExp_outerExpressionGrouped_optionalErrorsDisabled_doesNotFail() { + // This won't fail if optional errors are disabled. + expectSuccessWhenParsingNumericExpression("(7 * 2 + 4)", errorCheckingMode = REQUIRED_ONLY) + } - val failure38 = expectFailureWhenParsingAlgebraicExpression("--x") - assertThat(failure38).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + @Test + fun testParseNumExp_everythingDoubleParens_returnsMultiParensErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("((5 + 4))") + + assertThat(error).isMultipleRedundantParenthesesThat().apply { + // The valid sub-expression should be captured as part of the error. + hasRawExpressionThat().isEqualTo("(5 + 4)") + hasExpressionThat().hasStructureThatMatches { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } - val failure39 = expectFailureWhenParsingAlgebraicExpression("-+x") - assertThat(failure39).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + @Test + fun testParseNumExp_everythingTripleParens_returnsMultiParensErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("(((5 + 4)))") + + assertThat(error).isMultipleRedundantParenthesesThat().apply { + // The valid sub-expression should be captured as part of the error. + hasRawExpressionThat().isEqualTo("((5 + 4))") + hasExpressionThat().hasStructureThatMatches { + group { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + } - val failure40 = expectFailureWhenParsingNumericExpression("+-2") - assertThat(failure40).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + @Test + fun testParseNumExp_expWithDoubleParens_returnsMultiParensErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("1+((5 + 4))") + + assertThat(error).isMultipleRedundantParenthesesThat().apply { + // The valid sub-expression should be captured as part of the error. + hasRawExpressionThat().isEqualTo("(5 + 4)") + hasExpressionThat().hasStructureThatMatches { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } - parseNumericExpressionSuccessfully("2++3") // Will succeed since it's 2 + (+2). - val failure41 = expectFailureWhenParsingNumericExpression("2+++3") - assertThat(failure41).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + @Test + fun testParseNumExp_expWithTripleParens_returnsMultiParensErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("1+(7*((( 9 + 3) )))") + + assertThat(error).isMultipleRedundantParenthesesThat().apply { + // The valid sub-expression should be captured as part of the error. + hasRawExpressionThat().isEqualTo("(( 9 + 3) )") + hasExpressionThat().hasStructureThatMatches { + group { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(9) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } + } - val failure23 = expectFailureWhenParsingNumericExpression("/2") - assertThat(failure23).isInstanceOf(NoVariableOrNumberBeforeBinaryOperatorError::class.java) - assertThat((failure23 as NoVariableOrNumberBeforeBinaryOperatorError).operator) - .isEqualTo(MathBinaryOperation.Operator.DIVIDE) + @Test + fun testParseNumExp_expWithTripleParens_optionalErrorsDisabled_doesNotFail() { + // This doesn't trigger an error when optional errors are disabled. + expectSuccessWhenParsingNumericExpression( + "1+(7*((( 9 + 3) )))", errorCheckingMode = REQUIRED_ONLY + ) + } - val failure24 = expectFailureWhenParsingAlgebraicExpression("*x") - assertThat(failure24).isInstanceOf(NoVariableOrNumberBeforeBinaryOperatorError::class.java) - assertThat((failure24 as NoVariableOrNumberBeforeBinaryOperatorError).operator) - .isEqualTo(MathBinaryOperation.Operator.MULTIPLY) + @Test + fun testParseNumExp_innerExpWithSingleParens_onRight_doesNotFail() { + // Succeeds because the right parenthetical term is complex and is part of an outer expression. + expectSuccessWhenParsingNumericExpression("1+(5+4)") + } - val failure27 = expectFailureWhenParsingNumericExpression("2^") - assertThat(failure27).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) - assertThat((failure27 as NoVariableOrNumberAfterBinaryOperatorError).operator) - .isEqualTo(MathBinaryOperation.Operator.EXPONENTIATE) + @Test + fun testParseNumExp_innerExpWithSingleParens_onLeft_doesNotFail() { + // Succeeds because the left parenthetical term is complex and is part of an outer expression. + expectSuccessWhenParsingNumericExpression("(5+4)+1") + } - val failure25 = expectFailureWhenParsingNumericExpression("2/") - assertThat(failure25).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) - assertThat((failure25 as NoVariableOrNumberAfterBinaryOperatorError).operator) - .isEqualTo(MathBinaryOperation.Operator.DIVIDE) + @Test + fun testParseNumExp_numberSingleParen_onLeft_returnsSingleTermParenErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("(5) + 4") + + assertThat(error).isRedundantIndividualTermsParensThat().apply { + // The valid sub-expression should be captured as part of the error. + hasRawExpressionThat().isEqualTo("5") + hasExpressionThat().hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } - val failure26 = expectFailureWhenParsingAlgebraicExpression("x*") - assertThat(failure26).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) - assertThat((failure26 as NoVariableOrNumberAfterBinaryOperatorError).operator) - .isEqualTo(MathBinaryOperation.Operator.MULTIPLY) + @Test + fun testParseNumExp_numberSingleParen_onRight_returnsSingleTermParenErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("5^(2)") + + assertThat(error).isRedundantIndividualTermsParensThat().apply { + // The valid sub-expression should be captured as part of the error. + hasRawExpressionThat().isEqualTo("2") + hasExpressionThat().hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } - val failure28 = expectFailureWhenParsingAlgebraicExpression("x+") - assertThat(failure28).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) - assertThat((failure28 as NoVariableOrNumberAfterBinaryOperatorError).operator) - .isEqualTo(MathBinaryOperation.Operator.ADD) + @Test + fun testParseNumExp_numberSingleParen_sqrt_returnsSingleTermParenErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("sqrt((2))") + + assertThat(error).isRedundantIndividualTermsParensThat().apply { + // The valid sub-expression should be captured as part of the error. + hasRawExpressionThat().isEqualTo("2") + hasExpressionThat().hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } - val failure29 = expectFailureWhenParsingAlgebraicExpression("x-") - assertThat(failure29).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) - assertThat((failure29 as NoVariableOrNumberAfterBinaryOperatorError).operator) - .isEqualTo(MathBinaryOperation.Operator.SUBTRACT) + @Test + fun testParseNumExp_numSingleParen_betweenImplicitMult_returnsSingleTermParenErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("2^3(4)2^3") + + // While parentheses can enable implicit multiplication with numbers, due to exponents never + // being valid right implicit multiplication operands (for numeric expressions) the above is + // considered invalid (plus the '4' is by itself without anything else being in the group). + assertThat(error).isRedundantIndividualTermsParensThat().apply { + // The valid sub-expression should be captured as part of the error. + hasRawExpressionThat().isEqualTo("4") + hasExpressionThat().hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } - val failure42 = expectFailureWhenParsingAlgebraicExpression("2^x") - assertThat(failure42).isInstanceOf(ExponentIsVariableExpressionError::class.java) + @Test + fun testParseNumExp_numberSingleParen_sqrt_optionalErrorsDisabled_doesNotFail() { + // This doesn't trigger an error when optional errors are disabled. + expectSuccessWhenParsingNumericExpression("sqrt((2))", errorCheckingMode = REQUIRED_ONLY) + } - val failure43 = expectFailureWhenParsingAlgebraicExpression("2^(1+x)") - assertThat(failure43).isInstanceOf(ExponentIsVariableExpressionError::class.java) + @Test + fun testParseNumExp_dollarSign_returnsUnnecessarySymbolErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("$2") - val failure44 = expectFailureWhenParsingAlgebraicExpression("2^3^x") - assertThat(failure44).isInstanceOf(ExponentIsVariableExpressionError::class.java) + assertThat(error).isUnnecessarySymbolWithSymbolThat().isEqualTo("$") + } - val failure45 = expectFailureWhenParsingAlgebraicExpression("2^sqrt(x)") - assertThat(failure45).isInstanceOf(ExponentIsVariableExpressionError::class.java) + @Test + fun testParseNumExp_exclamation_returnsUnnecessarySymbolErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("5!") - val failure46 = expectFailureWhenParsingNumericExpression("2^7") - assertThat(failure46).isInstanceOf(ExponentTooLargeError::class.java) + assertThat(error).isUnnecessarySymbolWithSymbolThat().isEqualTo("!") + } - val failure47 = expectFailureWhenParsingNumericExpression("2^30.12") - assertThat(failure47).isInstanceOf(ExponentTooLargeError::class.java) + @Test + fun testParseAlgExp_unexpectedAmpersand_returnsUnnecessarySymbolErrorWithDetails() { + val error = expectFailureWhenParsingAlgebraicExpression("1+2 &xyz") - parseNumericExpressionSuccessfully("2^3") + assertThat(error).isUnnecessarySymbolWithSymbolThat().isEqualTo("&") + } - val failure48 = expectFailureWhenParsingNumericExpression("2^3^2") - assertThat(failure48).isInstanceOf(NestedExponentsError::class.java) + @Test + fun testParseAlgExp_numberRightOfVar_returnsNumberAfterVariableErrorWithDetails() { + val error = expectFailureWhenParsingAlgebraicExpression("x5") - val failure49 = expectFailureWhenParsingAlgebraicExpression("x^2^5") - assertThat(failure49).isInstanceOf(NestedExponentsError::class.java) + assertThat(error).isNumberAfterVariableThat().apply { + hasNumberThat().isIntegerThat().isEqualTo(5) + hasVariableThat().isEqualTo("x") + } + } - val failure20 = expectFailureWhenParsingNumericExpression("2√") - assertThat(failure20).isInstanceOf(HangingSquareRootError::class.java) + @Test + fun testParseAlgExp_expRightOfVar_returnsNumberAfterVariableErrorWithDetails() { + val error = expectFailureWhenParsingAlgebraicExpression("2+y 3.14*7") - val failure50 = expectFailureWhenParsingNumericExpression("2/0") - assertThat(failure50).isInstanceOf(TermDividedByZeroError::class.java) + assertThat(error).isNumberAfterVariableThat().apply { + hasNumberThat().isIrrationalThat().isWithin(1e-5).of(3.14) + hasVariableThat().isEqualTo("y") + } + } - val failure51 = expectFailureWhenParsingAlgebraicExpression("x/0") - assertThat(failure51).isInstanceOf(TermDividedByZeroError::class.java) + @Test + fun testParseAlgEq_numberRightOfVar_leftSide_returnsNumberAfterVariableErrorWithDetails() { + val error = expectFailureWhenParsingAlgebraicEquation("y 2 = (x+1)(x-1)") - val failure52 = expectFailureWhenParsingNumericExpression("sqrt(2+7/0.0)") - assertThat(failure52).isInstanceOf(TermDividedByZeroError::class.java) + assertThat(error).isNumberAfterVariableThat().apply { + hasNumberThat().isIntegerThat().isEqualTo(2) + hasVariableThat().isEqualTo("y") + } + } - val failure21 = expectFailureWhenParsingNumericExpression("x+y") - assertThat(failure21).isInstanceOf(VariableInNumericExpressionError::class.java) + @Test + @RunParameterized( + // Note that these parameters are intentionally set up to avoid double unary operators (such as + // -- or ++) since those result in different errors due to unary operations being higher + // precedence. In general, unary operators can't appear on the right since they'll be treated as + // such. + Iteration("**", "lhsOp=*", "rhsOp=*"), Iteration("×*", "lhsOp=×", "rhsOp=*"), + Iteration("/*", "lhsOp=/", "rhsOp=*"), Iteration("÷*", "lhsOp=÷", "rhsOp=*"), + Iteration("^*", "lhsOp=^", "rhsOp=*"), Iteration("+*", "lhsOp=+", "rhsOp=*"), + Iteration("-*", "lhsOp=-", "rhsOp=*"), Iteration("−*", "lhsOp=−", "rhsOp=*"), + Iteration("*×", "lhsOp=*", "rhsOp=×"), Iteration("××", "lhsOp=×", "rhsOp=×"), + Iteration("/×", "lhsOp=/", "rhsOp=×"), Iteration("÷×", "lhsOp=÷", "rhsOp=×"), + Iteration("^×", "lhsOp=^", "rhsOp=×"), Iteration("+×", "lhsOp=+", "rhsOp=×"), + Iteration("-×", "lhsOp=-", "rhsOp=×"), Iteration("−×", "lhsOp=−", "rhsOp=×"), + Iteration("*/", "lhsOp=*", "rhsOp=/"), Iteration("×/", "lhsOp=×", "rhsOp=/"), + Iteration("//", "lhsOp=/", "rhsOp=/"), Iteration("÷/", "lhsOp=÷", "rhsOp=/"), + Iteration("^/", "lhsOp=^", "rhsOp=/"), Iteration("+/", "lhsOp=+", "rhsOp=/"), + Iteration("-/", "lhsOp=-", "rhsOp=/"), Iteration("−/", "lhsOp=−", "rhsOp=/"), + Iteration("*÷", "lhsOp=*", "rhsOp=÷"), Iteration("×÷", "lhsOp=×", "rhsOp=÷"), + Iteration("/÷", "lhsOp=/", "rhsOp=÷"), Iteration("÷÷", "lhsOp=÷", "rhsOp=÷"), + Iteration("^÷", "lhsOp=^", "rhsOp=÷"), Iteration("+÷", "lhsOp=+", "rhsOp=÷"), + Iteration("-÷", "lhsOp=-", "rhsOp=÷"), Iteration("−÷", "lhsOp=−", "rhsOp=÷"), + Iteration("*^", "lhsOp=*", "rhsOp=^"), Iteration("×^", "lhsOp=×", "rhsOp=^"), + Iteration("/^", "lhsOp=/", "rhsOp=^"), Iteration("÷^", "lhsOp=÷", "rhsOp=^"), + Iteration("^^", "lhsOp=^", "rhsOp=^"), Iteration("+^", "lhsOp=+", "rhsOp=^"), + Iteration("-^", "lhsOp=-", "rhsOp=^"), Iteration("−^", "lhsOp=−", "rhsOp=^") + ) + fun testParseNumExp_adjacentBinaryOps_returnsSubsequentBinaryOperatorsErrorWithDetails() { + val expression = "1 $lhsOp$rhsOp 2" + + val error = expectFailureWhenParsingNumericExpression(expression) + + // Generally, two adjacent binary operators is an error since a value is expected between them. + assertThat(error).isSubsequentBinaryOperatorsThat().apply { + hasFirstOperatorThat().isEqualTo(lhsOp) + hasSecondOperatorThat().isEqualTo(rhsOp) + } + } - val failure53 = expectFailureWhenParsingAlgebraicExpression("x+y+a") - assertThat(failure53).isInstanceOf(DisabledVariablesInUseError::class.java) - assertThat((failure53 as DisabledVariablesInUseError).variables).containsExactly("a") + @Test + fun testParseNumExp_doubleUnaryPlus_returnsSubsequentUnaryOperatorsError() { + val error = expectFailureWhenParsingNumericExpression("++2") - val failure54 = expectFailureWhenParsingAlgebraicExpression("apple") - assertThat(failure54).isInstanceOf(DisabledVariablesInUseError::class.java) - assertThat((failure54 as DisabledVariablesInUseError).variables) - .containsExactly("a", "p", "l", "e") + assertThat(error).isSubsequentUnaryOperators() + } - val failure55 = - expectFailureWhenParsingAlgebraicExpression("apple", allowedVariables = listOf("a", "p", "l")) - assertThat(failure55).isInstanceOf(DisabledVariablesInUseError::class.java) - assertThat((failure55 as DisabledVariablesInUseError).variables).containsExactly("e") + @Test + fun testParseNumExp_doubleUnaryMinus_returnsSubsequentUnaryOperatorsError() { + val error = expectFailureWhenParsingAlgebraicExpression("--x") - parseAlgebraicExpressionSuccessfully("x+y+z") + assertThat(error).isSubsequentUnaryOperators() + } - val failure56 = - expectFailureWhenParsingAlgebraicExpression("x+y+z", allowedVariables = listOf()) - assertThat(failure56).isInstanceOf(DisabledVariablesInUseError::class.java) - assertThat((failure56 as DisabledVariablesInUseError).variables).containsExactly("x", "y", "z") + @Test + fun testParseNumExp_unaryMinusPlus_returnsSubsequentUnaryOperatorsError() { + val error = expectFailureWhenParsingAlgebraicExpression("-+x") - val failure30 = expectFailureWhenParsingAlgebraicEquation("x==2") - assertThat(failure30).isInstanceOf(EquationHasTooManyEqualsError::class.java) + assertThat(error).isSubsequentUnaryOperators() + } - val failure31 = expectFailureWhenParsingAlgebraicEquation("x=2=y") - assertThat(failure31).isInstanceOf(EquationHasTooManyEqualsError::class.java) + @Test + fun testParseNumExp_unaryPlusMinus_returnsSubsequentUnaryOperatorsError() { + val error = expectFailureWhenParsingNumericExpression("+-2") - val failure32 = expectFailureWhenParsingAlgebraicEquation("x=2=") - assertThat(failure32).isInstanceOf(EquationHasTooManyEqualsError::class.java) + assertThat(error).isSubsequentUnaryOperators() + } - val failure59 = expectFailureWhenParsingAlgebraicEquation("x") - assertThat(failure59).isInstanceOf(EquationIsMissingEqualsError::class.java) + @Test + fun testParseNumExp_twoMinuses_doesNotFail() { + expectSuccessWhenParsingNumericExpression("2--3") // Will succeed since it's 2 - (-2). + } - val failure33 = expectFailureWhenParsingAlgebraicEquation("x=") - assertThat(failure33).isInstanceOf(EquationMissingLhsOrRhsError::class.java) + @Test + fun testParseNumExp_threeMinuses_returnsSubsequentUnaryOperatorsError() { + val error = expectFailureWhenParsingNumericExpression("2---3") - val failure34 = expectFailureWhenParsingAlgebraicEquation("=x") - assertThat(failure34).isInstanceOf(EquationMissingLhsOrRhsError::class.java) + // The above results in this error since it's effectively "2 - (--3)", where the "--3" is + // invalid. + assertThat(error).isSubsequentUnaryOperators() + } - val failure35 = expectFailureWhenParsingAlgebraicEquation("=x") - assertThat(failure35).isInstanceOf(EquationMissingLhsOrRhsError::class.java) + @Test + fun testParseNumExp_threeMinuses_optionalErrorsDisabled_doesNotFail() { + // This doesn't trigger a failure when optional errors are disabled. + expectSuccessWhenParsingNumericExpression("2---3", errorCheckingMode = REQUIRED_ONLY) + } - // TODO: expand to multiple tests or use parametrized tests. - val prohibitedFunctionNames = - listOf( - "exp", "log", "log10", "ln", "sin", "cos", "tan", "cot", "csc", "sec", "atan", "asin", - "acos", "abs" - ) - for (functionName in prohibitedFunctionNames) { - val failure36 = expectFailureWhenParsingAlgebraicEquation("$functionName(0.5)") - assertThat(failure36).isInstanceOf(InvalidFunctionInUseError::class.java) - assertThat((failure36 as InvalidFunctionInUseError).functionName).isEqualTo(functionName) + @Test + @RunParameterized( + // Note that unary operators like '+' and '-' are excluded here since they may result in valid + // unary operations. + Iteration("nothing_times_something_asterisk", "binOp=*"), + Iteration("nothing_times_something", "binOp=×"), + Iteration("nothing_divides_something_slash", "binOp=/"), + Iteration("nothing_divides_something", "binOp=÷"), + Iteration("nothing_to_power_of_something", "binOp=^") + ) + fun testParseNumExp_binOnlyOps_noLeftValue_returnsNoVarOrNumBeforeBinOperatorErrorWithDetails() { + val expression = "$binOp 2" + val operator = BINARY_SYMBOL_TO_OPERATOR_MAP.getValue(binOp) + + val error = expectFailureWhenParsingNumericExpression(expression) + + // A binary operator with no left-hand side is invalid. + assertThat(error).isNoVarOrNumBeforeBinaryOperatorThat().apply { + hasOperatorThat().isEqualTo(operator) + hasOperatorSymbolThat().isEqualTo(binOp) } + } - val failure57 = expectFailureWhenParsingAlgebraicExpression("sq") - assertThat(failure57).isInstanceOf(FunctionNameIncompleteError::class.java) + @Test + fun testParseNumExp_unaryPlus_returnsNoVarOrNumBeforeBinOperatorErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("+2") + + // While '+2' is a valid unary expression, it's treated as an error (since it's more likely to + // be a mistyped binary operation than a no-side effect unary operation). + assertThat(error).isNoVarOrNumBeforeBinaryOperatorThat().apply { + hasOperatorThat().isEqualTo(ADD) + hasOperatorSymbolThat().isEqualTo("+") + } + } - val failure58 = expectFailureWhenParsingAlgebraicExpression("sqr") - assertThat(failure58).isInstanceOf(FunctionNameIncompleteError::class.java) + @Test + fun testParseNumExp_unaryPlus_optionalErrorsDisabled_doesNotFail() { + // This doesn't trigger a failure when optional errors are disabled. + expectSuccessWhenParsingNumericExpression("+2", errorCheckingMode = REQUIRED_ONLY) + } - // TODO: Other cases: sqrt(, sqrt(), sqrt 2, +2 + @Test + @RunParameterized( + // Note that unary operators like '+' and '-' are excluded here since they may result in valid + // unary operations. + Iteration("nothing_times_something_asterisk", "binOp=*"), + Iteration("nothing_times_something", "binOp=×"), + Iteration("nothing_divides_something_slash", "binOp=/"), + Iteration("nothing_divides_something", "binOp=÷"), + Iteration("nothing_to_power_of_something", "binOp=^") + ) + fun testParseAlgExp_binOnlyOps_noLeftValue_returnsNoVarOrNumBeforeBinOperatorErrorWithDetails() { + val expression = "$binOp x" + val operator = BINARY_SYMBOL_TO_OPERATOR_MAP.getValue(binOp) + + val error = expectFailureWhenParsingAlgebraicExpression(expression) + + // A binary operator with no left-hand side is invalid. + assertThat(error).isNoVarOrNumBeforeBinaryOperatorThat().apply { + hasOperatorThat().isEqualTo(operator) + hasOperatorSymbolThat().isEqualTo(binOp) + } } - private companion object { - // TODO: fix helper API. + @Test + @RunParameterized( + Iteration("something_times_nothing_asterisk", "binOp=*"), + Iteration("something_times_nothing", "binOp=×"), + Iteration("something_divides_nothing_slash", "binOp=/"), + Iteration("something_divides_nothing", "binOp=÷"), + Iteration("something_to_power_of_nothing", "binOp=^"), + Iteration("something_adds_nothing", "binOp=+"), + Iteration("something_subtracts_nothing_hyphen", "binOp=-"), + Iteration("something_subtracts_nothing", "binOp=−") + ) + fun testParseNumExp_binaryOps_noRightValue_returnsNoVarOrNumAfterBinOperatorErrorWithDetails() { + val expression = "2 $binOp" + val operator = BINARY_SYMBOL_TO_OPERATOR_MAP.getValue(binOp) + + val error = expectFailureWhenParsingNumericExpression(expression) + + // A binary operator with no right-hand side is invalid. + assertThat(error).isNoVariableOrNumberAfterBinaryOperatorThat().apply { + hasOperatorThat().isEqualTo(operator) + hasOperatorSymbolThat().isEqualTo(binOp) + } + } - private fun expectFailureWhenParsingNumericExpression(expression: String): MathParsingError { - val result = parseNumericExpressionWithAllErrors(expression) - assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) - return (result as MathParsingResult.Failure).error + @Test + @RunParameterized( + Iteration("something_times_nothing_asterisk", "binOp=*"), + Iteration("something_times_nothing", "binOp=×"), + Iteration("something_divides_nothing_slash", "binOp=/"), + Iteration("something_divides_nothing", "binOp=÷"), + Iteration("something_to_power_of_nothing", "binOp=^"), + Iteration("something_adds_nothing", "binOp=+"), + Iteration("something_subtracts_nothing_hyphen", "binOp=-"), + Iteration("something_subtracts_nothing", "binOp=−") + ) + fun testParseAlgExp_binaryOps_noRightValue_returnsNoVarOrNumAfterBinOperatorErrorWithDetails() { + val expression = "x $binOp" + val operator = BINARY_SYMBOL_TO_OPERATOR_MAP.getValue(binOp) + + val error = expectFailureWhenParsingAlgebraicExpression(expression) + + // A binary operator with no right-hand side is invalid. + assertThat(error).isNoVariableOrNumberAfterBinaryOperatorThat().apply { + hasOperatorThat().isEqualTo(operator) + hasOperatorSymbolThat().isEqualTo(binOp) } + } + + @Test + @RunParameterized( + Iteration("var_directly_in_exp", "subExp=x"), + Iteration("var_directly_in_sub_exp", "subExp=(1+x)"), + Iteration("var_directly_in_nested_exp", "subExp=3^x"), + Iteration("var_directly_in_sqrt", "subExp=sqrt(x)") + ) + fun testParseAlgExp_powersWithVariableExpressions_returnsExponentIsVariableExpressionError() { + val expression = "2^$subExp" + + val error = expectFailureWhenParsingAlgebraicExpression(expression) + + // Regardless of how a variable is within an exponent's power, it's always invalid. + assertThat(error).isExponentIsVariableExpression() + } + + @Test + fun testParseAlgExp_powersWithVariableExpression_optionalErrorsDisabled_doesNotFail() { + // This doesn't trigger a failure when optional errors are disabled. + expectSuccessWhenParsingAlgebraicExpression("2^x", errorCheckingMode = REQUIRED_ONLY) + } + + @Test + fun testParseNumExp_largeIntegerExponent_returnsExponentTooLargeError() { + val error = expectFailureWhenParsingNumericExpression("2^7") + + assertThat(error).isExponentTooLarge() + } - private fun parseNumericExpressionSuccessfully(expression: String): MathExpression { - val result = parseNumericExpressionWithAllErrors(expression) - return (result as MathParsingResult.Success).result + @Test + fun testParseNumExp_largeRealExponent_returnsExponentTooLargeError() { + val error = expectFailureWhenParsingNumericExpression("2^30.12") + + assertThat(error).isExponentTooLarge() + } + + @Test + fun testParseNumExp_largeRealExponent_optionalErrorsDisabled_doesNotFail() { + // This doesn't trigger a failure when optional errors are disabled. + expectSuccessWhenParsingNumericExpression("2^30.12", errorCheckingMode = REQUIRED_ONLY) + } + + @Test + fun testParseNumExp_smallIntegerExponent_doesNotFail() { + // Smaller exponents are fine. + expectSuccessWhenParsingNumericExpression("2^3") + } + + @Test + fun testParseNumExp_nestedExponents_returnsNestedExponentsError() { + val error = expectFailureWhenParsingNumericExpression("2^3^2") + + assertThat(error).isNestedExponents() + } + + @Test + fun testParseAlgExp_nestedExponents_returnsNestedExponentsError() { + val error = expectFailureWhenParsingAlgebraicExpression("x^2^5") + + assertThat(error).isNestedExponents() + } + + @Test + fun testParseAlgExp_nestedExponents_optionalErrorsDisabled_doesNotFail() { + // This doesn't trigger a failure when optional errors are disabled. + expectSuccessWhenParsingAlgebraicExpression("x^2^5", errorCheckingMode = REQUIRED_ONLY) + } + + @Test + fun testParseNumExp_noSquareRootArgumentForSymbol_returnsHangingSquareRootError() { + val error = expectFailureWhenParsingNumericExpression("2√") + + assertThat(error).isHangingSquareRoot() + } + + @Test + fun testParseNumExp_integerDividedByZero_returnsTermDividedByZeroError() { + val error = expectFailureWhenParsingNumericExpression("2/0") + + assertThat(error).isTermDividedByZero() + } + + @Test + fun testParseAlgExp_variableDividedByZero_returnsTermDividedByZeroError() { + val error = expectFailureWhenParsingAlgebraicExpression("x/0") + + assertThat(error).isTermDividedByZero() + } + + @Test + fun testParseNumExp_integerDividedByZeroInSqrt_returnsTermDividedByZeroError() { + val error = expectFailureWhenParsingNumericExpression("sqrt(2+7/0.0)") + + assertThat(error).isTermDividedByZero() + } + + @Test + fun testParseNumExp_integerDividedByZeroInSqrt_optionalErrorsDisabled_doesNotFail() { + // This doesn't trigger a failure when optional errors are disabled. + expectSuccessWhenParsingNumericExpression("sqrt(2+7/0.0)", errorCheckingMode = REQUIRED_ONLY) + } + + @Test + fun testParseNumExp_addVariables_returnsVariableInNumericExpressionError() { + val error = expectFailureWhenParsingNumericExpression("x+y") + + assertThat(error).isVariableInNumericExpression() + } + + @Test + fun testParseAlgExp_addUnsupportedVariable_returnsDisabledVariablesInUseErrorWithDetails() { + val allowedVariables = listOf("x", "y") + + val error = expectFailureWhenParsingAlgebraicExpression("x+y+a", allowedVariables) + + // 'a' isn't an allowed variable. + assertThat(error).isDisabledVariablesInUseWithVariablesThat().containsExactly("a") + } + + @Test + fun testParseAlgExp_multUnsupportedVariables_returnsDisabledVariablesInUseErrorWithDetails() { + val allowedVariables = listOf("x", "y") + + val error = expectFailureWhenParsingAlgebraicExpression("apple", allowedVariables) + + // All disabled variables should be considered. + assertThat(error) + .isDisabledVariablesInUseWithVariablesThat() + .containsExactly("a", "p", "l", "e") + } + + @Test + fun testParseAlgExp_multUnsupportedVariables_optionalErrorsDisabled_doesNotFail() { + val allowedVariables = listOf("x", "y") + + // This doesn't trigger a failure when optional errors are disabled. + expectSuccessWhenParsingAlgebraicExpression( + "apple", allowedVariables, errorCheckingMode = REQUIRED_ONLY + ) + } + + @Test + fun testParseAlgExp_addSupportedVariables_doesNotFail() { + val allowedVariables = listOf("x", "y", "a") + + // If only allowed variables are used, no errors should be reported. + expectSuccessWhenParsingAlgebraicExpression("x+y+a", allowedVariables) + } + + @Test + fun testParseAlgExp_addSupportedVariables_noneSupported_returnsDisabledVarsErrorWithDetails() { + val allowedVariables = listOf() + + val error = expectFailureWhenParsingAlgebraicExpression("x+y+z", allowedVariables) + + // No allowed variables essentially results in variables no longer being supported (though with + // less targeted errors than when using numeric expressions). + assertThat(error).isDisabledVariablesInUseWithVariablesThat().containsExactly("x", "y", "z") + } + + @Test + fun testParseAlgEq_twoEquals_returnsEquationHasTooManyEqualsError() { + val error = expectFailureWhenParsingAlgebraicEquation("x==2") + + assertThat(error).isEquationHasTooManyEquals() + } + + @Test + fun testParseAlgEq_doubleEquivalence_returnsEquationHasTooManyEqualsError() { + val error = expectFailureWhenParsingAlgebraicEquation("x=2=y") + + assertThat(error).isEquationHasTooManyEquals() + } + + @Test + fun testParseAlgEq_doubleEquivalence_missingSecondRhs_returnsEquationHasTooManyEqualsError() { + val error = expectFailureWhenParsingAlgebraicEquation("x=2=") + + assertThat(error).isEquationHasTooManyEquals() + } + + @Test + fun testParseAlgEq_noEquals_returnsEquationIsMissingEqualsError() { + val error = expectFailureWhenParsingAlgebraicEquation("x") + + assertThat(error).isEquationIsMissingEquals() + } + + @Test + fun testParseAlgEq_somethingEqualsNothing_returnsEquationMissingLhsOrRhsError() { + val error = expectFailureWhenParsingAlgebraicEquation("x=") + + assertThat(error).isEquationMissingLhsOrRhs() + } + + @Test + fun testParseAlgEq_nothingEqualsSomething_returnsEquationMissingLhsOrRhsError() { + val error = expectFailureWhenParsingAlgebraicEquation("=x") + + assertThat(error).isEquationMissingLhsOrRhs() + } + + @Test + @RunParameterized( + Iteration("exp", "func=exp"), Iteration("log", "func=log"), Iteration("log10", "func=log10"), + Iteration("ln", "func=ln"), Iteration("sin", "func=sin"), Iteration("cos", "func=cos"), + Iteration("tan", "func=tan"), Iteration("cot", "func=cot"), Iteration("csc", "func=csc"), + Iteration("sec", "func=sec"), Iteration("atan", "func=atan"), Iteration("asin", "func=asin"), + Iteration("acos", "func=acos"), Iteration("abs", "func=abs") + ) + fun testParseNumExp_prohibitedFunctionInUse_returnsInvalidFunctionInUseErrorWithDetails() { + val expression = "$func(0.5+1)" + + val error = expectFailureWhenParsingAlgebraicEquation(expression) + + // Usage of detected unsupported functions should result in failures. + assertThat(error).isInvalidFunctionInUseWithNameThat().isEqualTo(func) + } + + @Test + fun testParseAlgExp_unknownFunction_doesNotFail() { + val allowedVariables = LOWERCASE_LATIN_ALPHABET + + // An unknown function won't fail since, so long as it's not similar to known functions, it will + // be treated as implicit variable multiplication. This will fail if the letters composing the + // name are unsupported variables or if attempted in a numeric expression (since variables + // aren't supported). Finally, the '+1' avoids redundant parentheses errors. + expectSuccessWhenParsingAlgebraicExpression("round(2+1)", allowedVariables) + } + + @Test + @RunParameterized( + Iteration("ex", "func=ex"), Iteration("lo", "func=lo"), Iteration("log1", "func=log1"), + Iteration("si", "func=si"), Iteration("co", "func=co"), Iteration("ta", "func=ta"), + Iteration("cs", "func=cs"), Iteration("se", "func=se"), Iteration("at", "func=at"), + Iteration("ata", "func=ata"), Iteration("as", "func=as"), Iteration("asi", "func=asi"), + Iteration("ac", "func=ac"), Iteration("aco", "func=aco"), Iteration("ab", "func=ab"), + Iteration("sq", "func=sq"), Iteration("sqr", "func=sqr") + + ) + fun testParseAlgExp_startOfKnownFunction_returnsFunctionNameIncompleteError() { + val expression = "$func(0.5+1)" + val error = expectFailureWhenParsingAlgebraicExpression(expression) + + + // Starting a detected function but not completing it should result in an incomplete name error. + assertThat(error).isFunctionNameIncomplete() + } + + @Test + @RunParameterized( + Iteration("a", "func=a"), Iteration("c", "func=c"), Iteration("e", "func=e"), + Iteration("l", "func=l"), Iteration("s", "func=s"), Iteration("t", "func=t") + ) + fun testParseAlgExp_firstLetterOfKnownFunctions_areValidExpressions() { + val expression = "$func(0.5+1)" + val allowedVariables = LOWERCASE_LATIN_ALPHABET + + // The first letter of a function is just a variable (it's never treated as the start of a + // function name unless more letters are provided). + expectSuccessWhenParsingAlgebraicExpression(expression, allowedVariables) + } + + @Test + fun testParseNumExp_sqrtFunc_missingArgumentAndRightParen_returnsGenericError() { + val error = expectFailureWhenParsingNumericExpression("sqrt(") + + assertThat(error).isGenericError() + } + + @Test + fun testParseNumExp_sqrtFunc_missingArgument_returnsGenericError() { + val error = expectFailureWhenParsingNumericExpression("sqrt()") + + assertThat(error).isGenericError() + } + + @Test + fun testParseNumExp_sqrtFunc_missingParensWithFloatingArgument_returnsGenericError() { + val error = expectFailureWhenParsingNumericExpression("sqrt 2") + + assertThat(error).isGenericError() + } + + @Test + fun testParseAlgEq_extraNumberAtEnd_returnsGenericError() { + val error = expectFailureWhenParsingAlgebraicEquation("y = (x+1)(x-1) 2") + + // The trailing '2' isn't used in the expression. + assertThat(error).isGenericError() + } + + @Test + fun testParseAlgExp_hasEquals_returnsGenericError() { + val error = expectFailureWhenParsingAlgebraicExpression("x = √2 × 7 ÷ 4") + + // '=' is not allowed in algebraic expressions. + assertThat(error).isGenericError() + } + + @Test + fun testParseNumExp_trailingNumber_afterSqrt_returnsGenericError() { + val error = expectFailureWhenParsingNumericExpression("sqrt(2)3") + + // Right implicit multiplication of numbers isn't allowed. In this case, it's likely the '3' is + // being interpreted as an additional token that's unused. + assertThat(error).isGenericError() + } + + @Test + fun testParseNumExp_trailingImplicitlyMultipliedExponent_returnsGenericError() { + val error = expectFailureWhenParsingNumericExpression("sqrt(3) 2^2") + + // Right implicit multiplication of numeric exponents isn't allowed, though in this case it's + // likely that the '2^2' is being interpreted as additional, unused tokens. + assertThat(error).isGenericError() + } + + private companion object { + private val BINARY_SYMBOL_TO_OPERATOR_MAP = mapOf( + "*" to MULTIPLY, + "×" to MULTIPLY, + "/" to DIVIDE, + "÷" to DIVIDE, + "^" to EXPONENTIATE, + "+" to ADD, + "-" to SUBTRACT, + "−" to SUBTRACT + ) + private val LOWERCASE_LATIN_ALPHABET = listOf( + "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", + "t", "u", "v", "w", "x", "y", "z" + ) + + private fun expectSuccessWhenParsingNumericExpression( + expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + ) { + expectSuccessfulParsingResult(parseNumericExpression(expression, errorCheckingMode)) } - private fun parseNumericExpressionWithAllErrors( - expression: String - ): MathParsingResult { - return MathExpressionParser.parseNumericExpression(expression, ErrorCheckingMode.ALL_ERRORS) + private fun expectFailureWhenParsingNumericExpression(expression: String): MathParsingError { + return expectFailingParsingResult(parseNumericExpression(expression)) } - private fun expectFailureWhenParsingAlgebraicExpression( + private fun expectSuccessWhenParsingAlgebraicExpression( expression: String, - allowedVariables: List = listOf("x", "y", "z") + allowedVariables: List = listOf("x", "y", "z"), + errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + ) { + expectSuccessfulParsingResult( + parseAlgebraicExpression(expression, allowedVariables, errorCheckingMode) + ) + } + + private fun expectFailureWhenParsingAlgebraicExpression( + expression: String, allowedVariables: List = listOf("x", "y", "z") ): MathParsingError { - val result = - parseAlgebraicExpressionWithAllErrors(expression, allowedVariables) - assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) - return (result as MathParsingResult.Failure).error + return expectFailingParsingResult(parseAlgebraicExpression(expression, allowedVariables)) } - private fun parseAlgebraicExpressionSuccessfully( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathExpression { - val result = parseAlgebraicExpressionWithAllErrors(expression, allowedVariables) - return (result as MathParsingResult.Success).result + private fun expectSuccessWhenParsingAlgebraicEquation(expression: String) { + expectSuccessfulParsingResult(parseAlgebraicEquation(expression)) } - private fun parseAlgebraicExpressionWithAllErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") + private fun expectFailureWhenParsingAlgebraicEquation(expression: String): MathParsingError { + return expectFailingParsingResult(parseAlgebraicEquation(expression)) + } + + private fun parseNumericExpression( + expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS ): MathParsingResult { - return MathExpressionParser.parseAlgebraicExpression( - expression, allowedVariables, ErrorCheckingMode.ALL_ERRORS - ) + return MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) } - private fun expectFailureWhenParsingAlgebraicEquation(expression: String): MathParsingError { - val result = parseAlgebraicEquationInternal(expression, ErrorCheckingMode.ALL_ERRORS) - assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) - return (result as MathParsingResult.Failure).error + private fun parseAlgebraicExpression( + expression: String, allowedVariables: List, + errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables, errorCheckingMode + ) } - private fun parseAlgebraicEquationInternal( - expression: String, - errorCheckingMode: ErrorCheckingMode, - allowedVariables: List = listOf("x", "y", "z") - ): MathParsingResult { + private fun parseAlgebraicEquation(expression: String): MathParsingResult { return MathExpressionParser.parseAlgebraicEquation( - expression, allowedVariables, errorCheckingMode + expression, allowedVariables = listOf("x", "y", "z"), ALL_ERRORS ) } + + private fun expectSuccessfulParsingResult(result: MathParsingResult) { + assertThat(result).isInstanceOf(MathParsingResult.Success::class.java) + } + + private fun expectFailingParsingResult(result: MathParsingResult): MathParsingError { + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error + } } } diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index d1f9e17b47d..22bfefdea84 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -19,8 +19,6 @@ class NumericExpressionParserTest { fun testLotsOfCasesForNumericExpression() { // TODO: split this up // TODO: add log string generation for expressions. - expectFailureWhenParsingNumericExpression("") - val expression1 = parseNumericExpressionWithAllErrors("1") assertThat(expression1).hasStructureThatMatches { constant { @@ -28,8 +26,6 @@ class NumericExpressionParserTest { } } - expectFailureWhenParsingNumericExpression("x") - val expression2 = parseNumericExpressionWithAllErrors(" 2 ") assertThat(expression2).hasStructureThatMatches { constant { @@ -44,10 +40,6 @@ class NumericExpressionParserTest { } } - expectFailureWhenParsingNumericExpression(" x ") - - expectFailureWhenParsingNumericExpression(" z x ") - val expression4 = parseNumericExpressionWithoutOptionalErrors("2^3^2") assertThat(expression4).hasStructureThatMatches { exponentiation { @@ -163,10 +155,6 @@ class NumericExpressionParserTest { } } - expectFailureWhenParsingNumericExpression("sqr(2)") - - expectFailureWhenParsingNumericExpression("xyz(2)") - val expression6 = parseNumericExpressionWithAllErrors("732") assertThat(expression6).hasStructureThatMatches { constant { @@ -297,8 +285,6 @@ class NumericExpressionParserTest { } } - expectFailureWhenParsingNumericExpression("x = √2 × 7 ÷ 4") - val expression8 = parseNumericExpressionWithAllErrors("(1+2)(3+4)") assertThat(expression8).hasStructureThatMatches { multiplication { @@ -337,9 +323,6 @@ class NumericExpressionParserTest { } } - // Right implicit multiplication of numbers isn't allowed. - expectFailureWhenParsingNumericExpression("(1+2)2") - val expression10 = parseNumericExpressionWithAllErrors("2(1+2)") assertThat(expression10).hasStructureThatMatches { multiplication { @@ -367,9 +350,6 @@ class NumericExpressionParserTest { } } - // Right implicit multiplication of numbers isn't allowed. - expectFailureWhenParsingNumericExpression("sqrt(2)3") - val expression12 = parseNumericExpressionWithAllErrors("3sqrt(2)") assertThat(expression12).hasStructureThatMatches { multiplication { @@ -390,8 +370,6 @@ class NumericExpressionParserTest { } } - expectFailureWhenParsingNumericExpression("xsqrt(2)") - val expression13 = parseNumericExpressionWithAllErrors("sqrt(2)*(1+2)*(3-2^5)") assertThat(expression13).hasStructureThatMatches { multiplication { @@ -573,7 +551,7 @@ class NumericExpressionParserTest { } } - val expression18 = parseNumericExpressionWithAllErrors("1++4") + val expression18 = parseNumericExpressionWithoutOptionalErrors("1++4") assertThat(expression18).hasStructureThatMatches { addition { leftOperand { @@ -613,8 +591,6 @@ class NumericExpressionParserTest { } } - expectFailureWhenParsingNumericExpression("1-^-4") - val expression20 = parseNumericExpressionWithAllErrors("√2 × 7 ÷ 4") assertThat(expression20).hasStructureThatMatches { division { @@ -644,8 +620,6 @@ class NumericExpressionParserTest { } } - expectFailureWhenParsingNumericExpression("1+2 &asdf") - val expression21 = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(3)sqrt(4)") // Note that this tree demonstrates left associativity. assertThat(expression21).hasStructureThatMatches { @@ -927,13 +901,6 @@ class NumericExpressionParserTest { } } - // Numbers cannot have implicit multiplication unless they are in groups. - expectFailureWhenParsingNumericExpression("2 2") - - expectFailureWhenParsingNumericExpression("2 2^2") - - expectFailureWhenParsingNumericExpression("2^2 2") - val expression31 = parseNumericExpressionWithoutOptionalErrors("(3)(4)(5)") assertThat(expression31).hasStructureThatMatches { multiplication { @@ -1013,9 +980,6 @@ class NumericExpressionParserTest { } } - // An exponentiation can never be an implicit right operand. - expectFailureWhenParsingNumericExpression("2^(3)2^2") - val expression35 = parseNumericExpressionWithoutOptionalErrors("2^(3)*2^2") assertThat(expression35).hasStructureThatMatches { multiplication { @@ -1091,9 +1055,6 @@ class NumericExpressionParserTest { } } - // An exponentiation can never be an implicit right operand. - expectFailureWhenParsingNumericExpression("2^3(4)2^3") - val expression38 = parseNumericExpressionWithoutOptionalErrors("2^3(4)*2^3") assertThat(expression38).hasStructureThatMatches { // 2^3(4)*2^3 @@ -1148,14 +1109,6 @@ class NumericExpressionParserTest { } } - expectFailureWhenParsingNumericExpression("2^2 2^2") - expectFailureWhenParsingNumericExpression("(3) 2^2") - expectFailureWhenParsingNumericExpression("sqrt(3) 2^2") - expectFailureWhenParsingNumericExpression("√2 2^2") - expectFailureWhenParsingNumericExpression("2^2 3") - - expectFailureWhenParsingNumericExpression("-2 3") - val expression39 = parseNumericExpressionWithAllErrors("-(1+2)") assertThat(expression39).hasStructureThatMatches { negation { @@ -1178,9 +1131,6 @@ class NumericExpressionParserTest { } } - // Should pass for algebra. - expectFailureWhenParsingNumericExpression("-2 x") - val expression40 = parseNumericExpressionWithAllErrors("-2 (1+2)") assertThat(expression40).hasStructureThatMatches { // The negation happens last for parity with other common calculators. @@ -1590,12 +1540,6 @@ class NumericExpressionParserTest { } } - // Should fail for algebra. - expectFailureWhenParsingNumericExpression("x7") - - // Should pass for algebra. - expectFailureWhenParsingNumericExpression("2x^2") - val expression54 = parseNumericExpressionWithAllErrors("2*2/-4+7*2") assertThat(expression54).hasStructureThatMatches { // 2*2/-4+7*2 -> ((2*2)/(-4))+(7*2) @@ -1745,12 +1689,6 @@ class NumericExpressionParserTest { private companion object { // TODO: fix helper API. - private fun expectFailureWhenParsingNumericExpression(expression: String): MathParsingError { - val result = parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) - assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) - return (result as MathParsingResult.Failure).error - } - private fun parseNumericExpressionWithoutOptionalErrors(expression: String): MathExpression { val result = parseNumericExpressionInternal( From b7ac054a7dad6fb27ef79368a5976ce091afb924 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 21 Jan 2022 18:12:12 -0800 Subject: [PATCH 053/134] Finish algebraic equation tests. --- .../util/math/AlgebraicEquationParserTest.kt | 119 +++++++++++++----- 1 file changed, 87 insertions(+), 32 deletions(-) diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt index 8e94d04c379..afdc0588f00 100644 --- a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt @@ -1,38 +1,61 @@ package org.oppia.android.util.math -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat +import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode -/** Tests for [MathExpressionParser]. */ +/** + * Tests for [MathExpressionParser]. + * + * Note that this test suite specifically focuses on verifying that the parser can correctly parse + * algebraic equations (i.e. via [MathExpressionParser.parseAlgebraicEquation]. This suite is not as + * thorough as may be expected because: + * 1. It relies heavily on [AlgebraicExpressionParserTest] for verifying that algebraic expressions + * can be correctly parsed. This suite is mainly geared toward verifying that the parser + * essentially just parses expressions for each side of the equation. + * 2. Error cases are tested in [MathExpressionParserTest] instead of here, including for equations. + */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class AlgebraicEquationParserTest { @Test - fun testLotsOfCasesForAlgebraicEquation() { - val equation1 = parseAlgebraicEquationSuccessfully("x = 1") - assertThat(equation1).hasLeftHandSideThat().hasStructureThatMatches { + fun testParseAlgEq_simpleVariableAssignment_correctlyParsesBothSidesStructures() { + val equation = parseAlgebraicEquation("x = 1") + + assertThat(equation).hasLeftHandSideThat().hasStructureThatMatches { variable { withNameThat().isEqualTo("x") } } + assertThat(equation).hasRightHandSideThat().hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } - val equation2 = - parseAlgebraicEquationSuccessfully( + @Test + fun testParseAlgEq_slopeInterceptForm_additionalVars_correctlyParsesBothSidesStructures() { + val equation = + parseAlgebraicEquation( "y = mx + b", allowedVariables = listOf("x", "y", "b", "m") ) - assertThat(equation2).hasLeftHandSideThat().hasStructureThatMatches { + + assertThat(equation).hasLeftHandSideThat().hasStructureThatMatches { variable { withNameThat().isEqualTo("y") } } - assertThat(equation2).hasRightHandSideThat().hasStructureThatMatches { + assertThat(equation).hasRightHandSideThat().hasStructureThatMatches { addition { leftOperand { multiplication { @@ -55,14 +78,18 @@ class AlgebraicEquationParserTest { } } } + } - val equation3 = parseAlgebraicEquationSuccessfully("y = (x+1)^2") - assertThat(equation3).hasLeftHandSideThat().hasStructureThatMatches { + @Test + fun testParseAlgEq_binomialAssignedToY_correctlyParsesBothSidesStructures() { + val equation = parseAlgebraicEquation("y = (x+1)^2") + + assertThat(equation).hasLeftHandSideThat().hasStructureThatMatches { variable { withNameThat().isEqualTo("y") } } - assertThat(equation3).hasRightHandSideThat().hasStructureThatMatches { + assertThat(equation).hasRightHandSideThat().hasStructureThatMatches { exponentiation { leftOperand { group { @@ -87,14 +114,18 @@ class AlgebraicEquationParserTest { } } } + } - val equation4 = parseAlgebraicEquationSuccessfully("y = (x+1)(x-1)") - assertThat(equation4).hasLeftHandSideThat().hasStructureThatMatches { + @Test + fun testParseAlgEq_factoredPolynomialAssignedToY_correctlyParsesBothSidesStructures() { + val equation = parseAlgebraicEquation("y = (x+1)(x-1)") + + assertThat(equation).hasLeftHandSideThat().hasStructureThatMatches { variable { withNameThat().isEqualTo("y") } } - assertThat(equation4).hasRightHandSideThat().hasStructureThatMatches { + assertThat(equation).hasRightHandSideThat().hasStructureThatMatches { multiplication { leftOperand { group { @@ -130,12 +161,16 @@ class AlgebraicEquationParserTest { } } } + } - val equation5 = - parseAlgebraicEquationSuccessfully( + @Test + fun testParseAlgEq_generalLineEquation_onLeftSide_correctlyParsesBothSidesStructures() { + val equation = + parseAlgebraicEquation( "a*x^2 + b*x + c = 0", allowedVariables = listOf("x", "a", "b", "c") ) - assertThat(equation5).hasLeftHandSideThat().hasStructureThatMatches { + + assertThat(equation).hasLeftHandSideThat().hasStructureThatMatches { addition { leftOperand { addition { @@ -185,31 +220,51 @@ class AlgebraicEquationParserTest { } } } - assertThat(equation5).hasRightHandSideThat().hasStructureThatMatches { + assertThat(equation).hasRightHandSideThat().hasStructureThatMatches { constant { withValueThat().isIntegerThat().isEqualTo(0) } } } - private companion object { - // TODO: fix helper API. + @Test + fun testParseAlgEq_nonPolynomialEquation_correctlyParsesBothSidesStructures() { + val equation = parseAlgebraicEquation("x = 2^sqrt(3)") - private fun parseAlgebraicEquationSuccessfully( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathEquation { - val result = parseAlgebraicEquationWithAllErrors(expression, allowedVariables) - return (result as MathParsingResult.Success).result + assertThat(equation).hasLeftHandSideThat().hasStructureThatMatches { + variable { + withNameThat().isEqualTo("x") + } } + assertThat(equation).hasRightHandSideThat().hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } - private fun parseAlgebraicEquationWithAllErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathParsingResult { - return MathExpressionParser.parseAlgebraicEquation( + private companion object { + private fun parseAlgebraicEquation( + expression: String, allowedVariables: List = listOf("x", "y", "z") + ): MathEquation { + val result = MathExpressionParser.parseAlgebraicEquation( expression, allowedVariables, ErrorCheckingMode.ALL_ERRORS ) + assertThat(result).isInstanceOf(MathParsingResult.Success::class.java) + return (result as MathParsingResult.Success).result } } } From a122515dd564d0ef2663789cfee7b2e0165f7ddb Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 21 Jan 2022 20:35:15 -0800 Subject: [PATCH 054/134] Reimplement numeric expression tests. This is almost a full replacement. The new tests are more structured and intentional to cover key high-level concepts. More tests may be added in the future, but this is a sensible initial test offering. This also updates MathExpressionSubject to support checking specifically for implicit multiplication (and it's now required for such cases since explicit is otherwise assumed). --- .../testing/math/MathExpressionSubject.kt | 11 +- .../util/math/AlgebraicEquationParserTest.kt | 4 +- .../math/AlgebraicExpressionParserTest.kt | 2 + .../util/math/MathExpressionParserTest.kt | 15 +- .../util/math/NumericExpressionParserTest.kt | 2151 ++++++++--------- 5 files changed, 1029 insertions(+), 1154 deletions(-) diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt index c5eb730c868..674d110af7e 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt @@ -167,12 +167,19 @@ class MathExpressionSubject private constructor( * * This method will fail if the expression corresponding to the subject is not a multiplication * operation. See [BinaryOperationComparator] for example syntax. + * + * This verifies that the multiplication operation is explicit by default, and this behavior can + * be overwritten using [isImplicit]. */ - fun multiplication(init: BinaryOperationComparator.() -> Unit) { + fun multiplication(isImplicit: Boolean = false, init: BinaryOperationComparator.() -> Unit) { BinaryOperationComparator.createFromExpression( expression, expectedOperator = MathBinaryOperation.Operator.MULTIPLY - ).also(init) + ).also { + assertWithMessage( + "Expected multiplication to be ${if (isImplicit) "implicit" else "explicit" }" + ).that(expression.binaryOperation.isImplicit).isEqualTo(isImplicit) + }.also(init) } /** diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt index afdc0588f00..01f0aeddc27 100644 --- a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt @@ -58,7 +58,7 @@ class AlgebraicEquationParserTest { assertThat(equation).hasRightHandSideThat().hasStructureThatMatches { addition { leftOperand { - multiplication { + multiplication(isImplicit = true) { leftOperand { variable { withNameThat().isEqualTo("m") @@ -126,7 +126,7 @@ class AlgebraicEquationParserTest { } } assertThat(equation).hasRightHandSideThat().hasStructureThatMatches { - multiplication { + multiplication(isImplicit = true) { leftOperand { group { addition { diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt index e05d9c5906f..9a1957515bb 100644 --- a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt @@ -15,6 +15,8 @@ import org.robolectric.annotation.LooperMode @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class AlgebraicExpressionParserTest { + // TODO: finish docs. + @Test fun testLotsOfCasesForAlgebraicExpression() { // TODO: split this up diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt index f74e1db819a..4bfdb4199da 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -42,16 +42,11 @@ import org.robolectric.annotation.LooperMode @RunWith(OppiaParameterizedTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class MathExpressionParserTest { - @Parameter - lateinit var lhsOp: String - @Parameter - lateinit var rhsOp: String - @Parameter - lateinit var binOp: String - @Parameter - lateinit var subExp: String - @Parameter - lateinit var func: String + @Parameter lateinit var lhsOp: String + @Parameter lateinit var rhsOp: String + @Parameter lateinit var binOp: String + @Parameter lateinit var subExp: String + @Parameter lateinit var func: String @Test fun testParseNumExp_basicExpression_doesNotFail() { diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index 22bfefdea84..42971dc5d57 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -11,77 +11,93 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingM import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode -/** Tests for [MathExpressionParser]. */ +/** + * Tests for [MathExpressionParser]. + * + * This test suite specifically focuses on ensuring that fundamental numeric expressions, when + * parsed, yield the correct [MathExpression] structure that ensures proper order of operations (for + * both operator associativity and precedence). This suite does not cover errors (see + * [MathExpressionParserTest] for those tests), nor algebraic expressions (see + * [AlgebraicExpressionParserTest]). + */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class NumericExpressionParserTest { @Test - fun testLotsOfCasesForNumericExpression() { - // TODO: split this up - // TODO: add log string generation for expressions. - val expression1 = parseNumericExpressionWithAllErrors("1") - assertThat(expression1).hasStructureThatMatches { + fun testParse_singleInteger_returnsExpressionWithConstant() { + val expression = parseNumericExpressionWithAllErrors("1") + + assertThat(expression).hasStructureThatMatches { constant { withValueThat().isIntegerThat().isEqualTo(1) } } + } + + @Test + fun testParse_singleInteger_withWhitespace_returnsExpressionWithConstant() { + val expression = parseNumericExpressionWithAllErrors(" 2 ") - val expression2 = parseNumericExpressionWithAllErrors(" 2 ") - assertThat(expression2).hasStructureThatMatches { + assertThat(expression).hasStructureThatMatches { constant { withValueThat().isIntegerThat().isEqualTo(2) } } + } + + @Test + fun testParse_singleInteger_multipleDigits_returnsExpressionWithConstant() { + val expression = parseNumericExpressionWithAllErrors("732") + + assertThat(expression).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(732) + } + } + } + + @Test + fun testParse_singleRealNumber_withWhitespace_returnsExpressionWithConstant() { + val expression = parseNumericExpressionWithAllErrors(" 2.5 ") - val expression3 = parseNumericExpressionWithAllErrors(" 2.5 ") - assertThat(expression3).hasStructureThatMatches { + assertThat(expression).hasStructureThatMatches { constant { withValueThat().isIrrationalThat().isWithin(1e-5).of(2.5) } } + } - val expression4 = parseNumericExpressionWithoutOptionalErrors("2^3^2") - assertThat(expression4).hasStructureThatMatches { - exponentiation { + @Test + fun testParse_addition_returnsExpressionWithBinaryOperation() { + val expression = parseNumericExpressionWithAllErrors("1 + 2") + + assertThat(expression).hasStructureThatMatches { + addition { leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } } } + } - val expression23 = parseNumericExpressionWithAllErrors("(2^3)^2") - assertThat(expression23).hasStructureThatMatches { - exponentiation { + @Test + fun testParse_subtraction_returnsExpressionWithBinaryOperation() { + val expression = parseNumericExpressionWithAllErrors("1 - 2") + + assertThat(expression).hasStructureThatMatches { + subtraction { leftOperand { - group { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { @@ -91,749 +107,245 @@ class NumericExpressionParserTest { } } } + } - val expression24 = parseNumericExpressionWithAllErrors("512/32/4") - assertThat(expression24).hasStructureThatMatches { - division { + @Test + fun testParse_subtraction_withMathSymbol_returnsExpressionWithBinaryOperation() { + val expression = parseNumericExpressionWithAllErrors("1 − 2") + + assertThat(expression).hasStructureThatMatches { + subtraction { leftOperand { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(512) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(32) - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { constant { - withValueThat().isIntegerThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(2) } } } } + } - val expression25 = parseNumericExpressionWithAllErrors("512/(32/4)") - assertThat(expression25).hasStructureThatMatches { - division { + @Test + fun testParse_multiplication_returnsExpressionWithBinaryOperation() { + val expression = parseNumericExpressionWithAllErrors("1 * 2") + + assertThat(expression).hasStructureThatMatches { + multiplication { leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(512) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { - group { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(32) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } } } + } - val expression5 = parseNumericExpressionWithAllErrors("sqrt(2)") - assertThat(expression5).hasStructureThatMatches { - functionCallTo(SQUARE_ROOT) { - argument { + @Test + fun testParse_multiplication_withMathSymbol_returnsExpressionWithBinaryOperation() { + val expression = parseNumericExpressionWithAllErrors("1 × 2") + + assertThat(expression).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { constant { withValueThat().isIntegerThat().isEqualTo(2) } } } } + } - val expression6 = parseNumericExpressionWithAllErrors("732") - assertThat(expression6).hasStructureThatMatches { - constant { - withValueThat().isIntegerThat().isEqualTo(732) - } - } + @Test + fun testParse_division_returnsExpressionWithBinaryOperation() { + val expression = parseNumericExpressionWithAllErrors("1 / 2") - // Verify order of operations between higher & lower precedent operators. - val expression32 = parseNumericExpressionWithAllErrors("3+4^5") - assertThat(expression32).hasStructureThatMatches { - addition { + assertThat(expression).hasStructureThatMatches { + division { leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } } } + } - val expression7 = parseNumericExpressionWithoutOptionalErrors("3*2-3+4^7*8/3*2+7") - assertThat(expression7).hasStructureThatMatches { - // To better visualize the precedence & order of operations, see this grouped version: - // (((3*2)-3)+((((4^7)*8)/3)*2))+7. - addition { + @Test + fun testParse_division_withMathSymbol_returnsExpressionWithBinaryOperation() { + val expression = parseNumericExpressionWithAllErrors("1 ÷ 2") + + assertThat(expression).hasStructureThatMatches { + division { leftOperand { - // ((3*2)-3)+((((4^7)*8)/3)*2) - addition { - leftOperand { - // (1*2)-3 - subtraction { - leftOperand { - // 3*2 - multiplication { - leftOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - rightOperand { - // (((4^7)*8)/3)*2 - multiplication { - leftOperand { - // ((4^7)*8)/3 - division { - leftOperand { - // (4^7)*8 - multiplication { - leftOperand { - // 4^7 - exponentiation { - leftOperand { - // 4 - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - rightOperand { - // 7 - constant { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - } - } - rightOperand { - // 8 - constant { - withValueThat().isIntegerThat().isEqualTo(8) - } - } - } - } - rightOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - rightOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { - // 7 constant { - withValueThat().isIntegerThat().isEqualTo(7) + withValueThat().isIntegerThat().isEqualTo(2) } } } } + } - val expression8 = parseNumericExpressionWithAllErrors("(1+2)(3+4)") - assertThat(expression8).hasStructureThatMatches { - multiplication { + @Test + fun testParse_exponentiation_returnsExpressionWithBinaryOperation() { + val expression = parseNumericExpressionWithAllErrors("1 ^ 2") + + assertThat(expression).hasStructureThatMatches { + exponentiation { leftOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } } } + } - val expression10 = parseNumericExpressionWithAllErrors("2(1+2)") - assertThat(expression10).hasStructureThatMatches { - multiplication { - leftOperand { + @Test + fun testParse_negation_returnsExpressionWithUnaryOperation() { + val expression = parseNumericExpressionWithAllErrors("-2") + + assertThat(expression).hasStructureThatMatches { + negation { + operand { constant { withValueThat().isIntegerThat().isEqualTo(2) } } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - - val expression12 = parseNumericExpressionWithAllErrors("3sqrt(2)") - assertThat(expression12).hasStructureThatMatches { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - - val expression13 = parseNumericExpressionWithAllErrors("sqrt(2)*(1+2)*(3-2^5)") - assertThat(expression13).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - rightOperand { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - } - } - } - } - } - } - } - - val expression58 = parseNumericExpressionWithAllErrors("sqrt(2)(1+2)(3-2^5)") - assertThat(expression58).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - rightOperand { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - } - } - } - } - } } } + } - val expression14 = parseNumericExpressionWithoutOptionalErrors("((3))") - assertThat(expression14).hasStructureThatMatches { - group { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } + @Test + fun testParse_positiveUnary_withoutOptionalErrors_returnsExpressionWithUnaryOperation() { + val expression = parseNumericExpressionWithoutOptionalErrors("+2") - val expression15 = parseNumericExpressionWithoutOptionalErrors("++3") - assertThat(expression15).hasStructureThatMatches { + assertThat(expression).hasStructureThatMatches { positive { operand { - positive { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - } - - val expression16 = parseNumericExpressionWithoutOptionalErrors("--4") - assertThat(expression16).hasStructureThatMatches { - negation { - operand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - - val expression17 = parseNumericExpressionWithAllErrors("1+-4") - assertThat(expression17).hasStructureThatMatches { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - - val expression18 = parseNumericExpressionWithoutOptionalErrors("1++4") - assertThat(expression18).hasStructureThatMatches { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - positive { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - - val expression19 = parseNumericExpressionWithAllErrors("1--4") - assertThat(expression19).hasStructureThatMatches { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - - val expression20 = parseNumericExpressionWithAllErrors("√2 × 7 ÷ 4") - assertThat(expression20).hasStructureThatMatches { - division { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - - val expression21 = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(3)sqrt(4)") - // Note that this tree demonstrates left associativity. - assertThat(expression21).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - - val expression22 = parseNumericExpressionWithAllErrors("(1+2)(3-7^2)(5+-17)") - assertThat(expression22).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - // 1+2 - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - rightOperand { - // 3-7^2 - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - } - } - rightOperand { - // 5+-17 - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(17) - } - } - } - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } } } + } - val expression26 = parseNumericExpressionWithAllErrors("3^-2") - assertThat(expression26).hasStructureThatMatches { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } + @Test + fun testParse_integerInParentheses_withoutOptionalErrors_returnsExpressionWithGroup() { + val expression = parseNumericExpressionWithoutOptionalErrors("(2)") + + assertThat(expression).hasStructureThatMatches { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } } + } - val expression27 = parseNumericExpressionWithoutOptionalErrors("(3^-2)^(3^-2)") - assertThat(expression27).hasStructureThatMatches { - exponentiation { - leftOperand { - group { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } + @Test + fun testParse_inlineSquareRoot_returnsExpressionWithFunctionCall() { + val expression = parseNumericExpressionWithAllErrors("√2") + + assertThat(expression).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } - rightOperand { - group { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } + } + } + } + + @Test + fun testParse_explicitSquareRoot_returnsExpressionWithFunctionCall() { + val expression = parseNumericExpressionWithAllErrors("sqrt(2)") + + assertThat(expression).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } } } + } - val expression28 = parseNumericExpressionWithAllErrors("1-3^sqrt(4)") - assertThat(expression28).hasStructureThatMatches { - subtraction { + @Test + fun testParse_multiplicationAndAddition_returnsExpWithMultResolvedFirst() { + val expression = parseNumericExpressionWithAllErrors("1+2*3") + + // Multiplication is resolved first since it's higher precedence. + assertThat(expression).hasStructureThatMatches { + addition { leftOperand { constant { withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { - exponentiation { + multiplication { leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(3) } } } } } } + } + + @Test + fun testParse_exponentiationAndMultiplication_returnsExpWithExponentsResolvedFirst() { + val expression = parseNumericExpressionWithAllErrors("2*3^4") - // "Hard" order of operation problems loosely based on & other problems that can often stump - // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. - val expression29 = parseNumericExpressionWithAllErrors("3÷2*(3+4)") - assertThat(expression29).hasStructureThatMatches { + // Exponentiation is resolved first since it's higher precedence. + assertThat(expression).hasStructureThatMatches { multiplication { leftOperand { - division { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + exponentiation { leftOperand { constant { withValueThat().isIntegerThat().isEqualTo(3) @@ -841,319 +353,294 @@ class NumericExpressionParserTest { } rightOperand { constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } + withValueThat().isIntegerThat().isEqualTo(4) } } } } } } + } - val expression59 = parseNumericExpressionWithAllErrors("3÷2(3+4)") - assertThat(expression59).hasStructureThatMatches { - multiplication { + @Test + fun testParse_groupAndExponentiation_returnsExpWithGroupResolvedFirst() { + val expression = parseNumericExpressionWithAllErrors("(2*3)^4") + + // Exponentiation is resolved last since the group is higher precedence. + assertThat(expression).hasStructureThatMatches { + exponentiation { leftOperand { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { group { - addition { + multiplication { leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - - val expression31 = parseNumericExpressionWithoutOptionalErrors("(3)(4)(5)") - assertThat(expression31).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - group { constant { withValueThat().isIntegerThat().isEqualTo(3) } } } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } } } rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - } - } - } - - val expression33 = parseNumericExpressionWithoutOptionalErrors("2^(3)") - assertThat(expression33).hasStructureThatMatches { - exponentiation { - leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } + withValueThat().isIntegerThat().isEqualTo(4) } } } } + } - // Verify that implicit multiple has lower precedence than exponentiation. - val expression34 = parseNumericExpressionWithoutOptionalErrors("2^(3)(4)") - assertThat(expression34).hasStructureThatMatches { - multiplication { - leftOperand { + @Test + fun testParse_negationAndExponentiation_returnsExpWithNegationResolvedLast() { + val expression = parseNumericExpressionWithAllErrors("-3^4") + + // Exponentiation is resolved first since negation is lower precedent. + assertThat(expression).hasStructureThatMatches { + negation { + operand { exponentiation { leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } + constant { + withValueThat().isIntegerThat().isEqualTo(4) } } } } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } } } + } - val expression35 = parseNumericExpressionWithoutOptionalErrors("2^(3)*2^2") - assertThat(expression35).hasStructureThatMatches { - multiplication { + @Test + fun testParse_inlineSquareRootAndExponentiation_returnsExpWithSquareRootResolvedFirst() { + val expression = parseNumericExpressionWithAllErrors("√3^4") + + // The square root is resolved first since it's higher precedence. + assertThat(expression).hasStructureThatMatches { + exponentiation { leftOperand { - exponentiation { - leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } + withValueThat().isIntegerThat().isEqualTo(3) } } } } rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(4) } } } } + } - // An exponentiation can be a right operand of an implicit mult if it's grouped. - val expression36 = parseNumericExpressionWithoutOptionalErrors("2^(3)(2^2)") - assertThat(expression36).hasStructureThatMatches { - multiplication { + @Test + fun testParse_additionAndSubtraction_returnsExpWithBothAtSamePrecedenceAndLeftAssociative() { + val expression = parseNumericExpressionWithAllErrors("1+2-3+4-5") + + // Addition and subtraction are resolved in-order since they're the same precedence, but they're + // resolved with left associativity (that is, left-to-right). The above expression can have its + // associativity made clearer with grouping: (((1+2)-3)+4)-5. + assertThat(expression).hasStructureThatMatches { + subtraction { leftOperand { - exponentiation { + addition { leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + subtraction { + leftOperand { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } } } rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } + constant { + withValueThat().isIntegerThat().isEqualTo(4) } } } } rightOperand { - group { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(5) } } } } + } - val expression38 = parseNumericExpressionWithoutOptionalErrors("2^3(4)*2^3") - assertThat(expression38).hasStructureThatMatches { - // 2^3(4)*2^3 - multiplication { + @Test + fun testParse_multiplicationAndDivision_returnsExpWithBothAtSamePrecedenceAndLeftAssociative() { + val expression = parseNumericExpressionWithAllErrors("2*3/4*5/6") + + // Multiplication and division are resolved in-order since they're the same precedence, but + // they're resolved with left associativity (that is, left-to-right). The above expression can + // have its associativity made clearer with grouping: (((2*3)/4)*5)/6. + assertThat(expression).hasStructureThatMatches { + division { leftOperand { - // 2^3(4) multiplication { leftOperand { - // 2^3 - exponentiation { + division { leftOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } } } rightOperand { - // 3 constant { - withValueThat().isIntegerThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(4) } } } } rightOperand { - // 4 - group { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } + constant { + withValueThat().isIntegerThat().isEqualTo(5) } } } } rightOperand { - // 2^3 + constant { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + } + } + } + + @Test + fun testParse_nestedExponents_returnsExpWithExponentsAsRightAssociative() { + val expression = parseNumericExpressionWithoutOptionalErrors("2^3^4") + + // Exponentiation is resolved with right associativity, that is, from right to left. This is + // made clearer by grouping: 2^(3^4). Note that this is a specific choice made by the + // implementation as there's no broad consensus around exponentiation associativity for infix + // exponentiation. Right associativity is ideal since it more closely matches written-out + // exponentiation (where the nested exponent is resolved first). + assertThat(expression).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { exponentiation { leftOperand { - // 2 constant { - withValueThat().isIntegerThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { - // 3 constant { - withValueThat().isIntegerThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(4) } } } } } } + } - val expression39 = parseNumericExpressionWithAllErrors("-(1+2)") - assertThat(expression39).hasStructureThatMatches { - negation { - operand { + @Test + fun testParse_nestedExponents_withGroups_returnsExpWithForcedLeftAssociativeExponent() { + val expression = parseNumericExpressionWithAllErrors("(2^3)^4") + + // Nested exponentiation can be "forced" to be left-associative by using a group to explicitly + // change the order (since groups have higher precedence than exponents). + assertThat(expression).hasStructureThatMatches { + exponentiation { + leftOperand { group { - addition { + exponentiation { leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { constant { - withValueThat().isIntegerThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(3) } } } } } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } } } + } - val expression40 = parseNumericExpressionWithAllErrors("-2 (1+2)") - assertThat(expression40).hasStructureThatMatches { - // The negation happens last for parity with other common calculators. - negation { - operand { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + @Test + fun testParse_negationAndInlineSquareRoot_returnsExpWithBothResolvedWithRightAssociativity() { + val expression = parseNumericExpressionWithAllErrors("√-13+-√17") + + // This expression demonstrates a few things: + // 1. Combining binary and unary operators (to demonstrate relative precedence). + // 2. That square roots are demonstrated with right associativity (i.e. the inner operator + // happens first). + assertThat(expression).hasStructureThatMatches { + addition { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(13) + } + } } } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } + } + } + rightOperand { + negation { + operand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(17) } } } @@ -1162,57 +649,76 @@ class NumericExpressionParserTest { } } } + } - val expression41 = parseNumericExpressionWithoutOptionalErrors("-2^3(4)") - assertThat(expression41).hasStructureThatMatches { - negation { + @Test + fun testParse_multipleNegAndPosOps_noOptionalErrors_returnsExpWithRightAssociativeUnaryOps() { + val expression = parseNumericExpressionWithoutOptionalErrors("+--++-3") + + // This demonstrates that unary operators are resolved with right associativity (i.e. + // right-to-left with the innermost operator resolving first). + assertThat(expression).hasStructureThatMatches { + positive { operand { - multiplication { - leftOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) + negation { + operand { + negation { + operand { + positive { + operand { + positive { + operand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } } } } } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } } } } } + } - val expression43 = parseNumericExpressionWithoutOptionalErrors("√2^2(3)") - assertThat(expression43).hasStructureThatMatches { - multiplication { + @Test + fun testParse_explicitMultiplication_returnsExpressionThatDoesNotHaveImplicitMultiplication() { + val expression = parseNumericExpressionWithAllErrors("2 * 3") + + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = false) { leftOperand { - exponentiation { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + @Test + fun testParse_twoAdjacentNumbers_withGroup_withoutOptionalErrors_returnsExpWithImplicitMult() { + // This isn't valid without turning off extra error detecting since redundant parentheses (i.e. + // the "(3)") trigger an error. + val expression = parseNumericExpressionWithoutOptionalErrors("2(3)") + + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { @@ -1224,32 +730,31 @@ class NumericExpressionParserTest { } } } + } - val expression60 = parseNumericExpressionWithoutOptionalErrors("√(2^2(3))") - assertThat(expression60).hasStructureThatMatches { - functionCallTo(SQUARE_ROOT) { - argument { + @Test + fun testParse_numberNextToParentheses_returnsExpWithImplicitMultiplication() { + val expression = parseNumericExpressionWithAllErrors("2(1+2)") + + // The parentheses indicate implicit multiplication. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { group { - multiplication { + addition { leftOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1257,26 +762,42 @@ class NumericExpressionParserTest { } } } + } - val expression42 = parseNumericExpressionWithAllErrors("-2*-2") - // Note that the following structure is not the same as (-2)*(-2) since unary negation has - // higher precedence than multiplication, so it's first & recurses to include the entire - // multiplication expression. - assertThat(expression42).hasStructureThatMatches { - negation { - operand { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + @Test + fun testParse_twoAdjacentParentheticalAdditions_returnsExpWithImplicitlyMultipliedSubExps() { + val expression = parseNumericExpressionWithAllErrors("(1+2)(3+4)") + + // Two adjacent expressions may sometimes be considered implicitly multiplied. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { + leftOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -1284,50 +805,69 @@ class NumericExpressionParserTest { } } } + } - // TODO: Here & elsewhere, fix the fact that this is actually a valid use of single-term - // parentheses (there's a bug in the current error detection logic). - val expression44 = parseNumericExpressionWithoutOptionalErrors("2(2)") - assertThat(expression44).hasStructureThatMatches { - multiplication { + @Test + fun testParse_numberNextToInlineSquareRoot_returnsExpWithImplicitMultiplication() { + val expression = parseNumericExpressionWithAllErrors("2√3") + + // Square roots are treated as markers for implicit multiplication. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { leftOperand { constant { withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } } } } } } + } - val expression45 = parseNumericExpressionWithAllErrors("2sqrt(2)") - assertThat(expression45).hasStructureThatMatches { - multiplication { + @Test + fun testParse_twoAdjacentInlineSquareRoots_returnsExpWithImplicitMultiplication() { + val expression = parseNumericExpressionWithAllErrors("√2√3") + + // Square roots are treated as markers for implicit multiplication. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } } } rightOperand { functionCallTo(SQUARE_ROOT) { argument { constant { - withValueThat().isIntegerThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(3) } } } } } } + } - val expression46 = parseNumericExpressionWithAllErrors("2√2") - assertThat(expression46).hasStructureThatMatches { - multiplication { + @Test + fun testParse_numberNextToSquareRoot_returnsExpWithImplicitMultiplication() { + val expression = parseNumericExpressionWithAllErrors("2sqrt(2)") + + // Functions are treated as markers for implicit multiplication. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { leftOperand { constant { withValueThat().isIntegerThat().isEqualTo(2) @@ -1344,30 +884,45 @@ class NumericExpressionParserTest { } } } + } - val expression47 = parseNumericExpressionWithoutOptionalErrors("(2)(2)") - assertThat(expression47).hasStructureThatMatches { - multiplication { + @Test + fun testParse_twoAdjacentSquareRoots_returnsExpWithImplicitMultiplication() { + val expression = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(2)") + + // Functions are treated as markers for implicit multiplication. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { leftOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } } } } rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } } } } } } + } - val expression48 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)(2)") - assertThat(expression48).hasStructureThatMatches { - multiplication { + @Test + fun testParse_squareRootNextToNumber_withoutOptionalErrors_returnsExpWithImplicitMult() { + val expression = parseNumericExpressionWithoutOptionalErrors("sqrt(2)(3)") + + // The parser recognizes this case as implicit multiplication, but additional errors are + // triggered since the "(3)" has redundant parentheses. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { leftOperand { functionCallTo(SQUARE_ROOT) { argument { @@ -1380,111 +935,382 @@ class NumericExpressionParserTest { rightOperand { group { constant { - withValueThat().isIntegerThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(3) } } } } } + } - val expression49 = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(2)") - assertThat(expression49).hasStructureThatMatches { - multiplication { + @Test + fun testParse_multipleAdjacentInlineSquareRoots_returnsExpWithLeftAssociativeImplicitMult() { + val expression = parseNumericExpressionWithAllErrors("√2√3√4") + + // Implicit multiplication is left-associative, i.e.: (√2*√3)*√4. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { leftOperand { + multiplication(isImplicit = true) { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + rightOperand { functionCallTo(SQUARE_ROOT) { argument { constant { - withValueThat().isIntegerThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(4) } } } } + } + } + } + + @Test + fun testParse_implicitMultiplicationAndAddition_returnsExpWithImplicitMultAtHigherPrecedence() { + val expression = parseNumericExpressionWithAllErrors("1+3√2") + + // Implicit multiplication is higher precedence so it's evaluated first. + assertThat(expression).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { + multiplication(isImplicit = true) { + leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } } } } } } } + } - val expression50 = parseNumericExpressionWithAllErrors("√2√2") - assertThat(expression50).hasStructureThatMatches { + @Test + fun testParse_implicitMultiplicationAndDivision_returnsExpWithSamePrecedence() { + // "Hard" order of operation problem loosely based on & other problems that can often stump + // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html, and that + // can also break parsers that incorrectly set implicit multiplication to a higher precedence. + val expression = parseNumericExpressionWithAllErrors("3÷2(3+4)*7") + + // The parser should ensure that implicit multiplication & explicit multiplication/division are + // the same precedence to ensure that evaluation is predictable. If implicit multiplication is + // higher precedence, then the expression above would be evaluated 0.0306 to rather than 73.5 + // (the correct value per multiple calculators). Below demonstrates this by showing that + // implicit multiplication follows the same associative rules as explicit + // multiplication/division (and not taking a higher-priority execution order). For simplicity, + // the expression above can be thought of as the equivalent: ((3÷2)*(3+4))*7. + assertThat(expression).hasStructureThatMatches { multiplication { leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { + multiplication(isImplicit = true) { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + } + + @Test + fun testParse_implicitMultAndExponents_noOptionalErrors_returnsExpWithImplicitMultEvaledSecond() { + val expression = parseNumericExpressionWithoutOptionalErrors("2^(3)(4)") + + // Implicit multiplication is lower precedent than exponentiation (and thus evaluated last). + // Note that the expression above violates the redundant parentheses check hence why optional + // errors are disabled. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { + leftOperand { + exponentiation { + leftOperand { constant { withValueThat().isIntegerThat().isEqualTo(2) } } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } } } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + @Test + fun testParse_adjacentExponentsWithGroup_returnsExpWithImplicitMult() { + val expression = parseNumericExpressionWithAllErrors("2^3(4^5)") + + // Two adjacent exponentiations can be implicitly multiplied if one is grouped. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { + leftOperand { + exponentiation { + leftOperand { constant { withValueThat().isIntegerThat().isEqualTo(2) } } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } } } } } + } - val expression51 = parseNumericExpressionWithoutOptionalErrors("(2)(2)(2)") - assertThat(expression51).hasStructureThatMatches { - multiplication { + @Test + fun testParse_complexExpression_returnsExpWithCorrectOrderOfOperations() { + val expression = parseNumericExpressionWithAllErrors("3*2-3+4^3*8/3*2+7") + + assertThat(expression).hasStructureThatMatches { + // To better visualize the precedence & order of operations, see this grouped version: + // (((3*2)-3)+((((4^3)*8)/3)*2))+7. + addition { leftOperand { - multiplication { + // ((3*2)-3)+((((4^3)*8)/3)*2) + addition { leftOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + // (1*2)-3 + subtraction { + leftOperand { + // 3*2 + multiplication { + leftOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // (((4^3)*8)/3)*2 + multiplication { + leftOperand { + // ((4^3)*8)/3 + division { + leftOperand { + // (4^3)*8 + multiplication { + leftOperand { + // 4^3 + exponentiation { + leftOperand { + // 4 + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // 8 + constant { + withValueThat().isIntegerThat().isEqualTo(8) + } + } + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } } } } } } rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) } } } } + } - val expression52 = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(2)sqrt(2)") - assertThat(expression52).hasStructureThatMatches { - multiplication { + @Test + fun testParse_compoundExpressionWithMultipleOperations_returnsExpWithCorrectOperationOrder() { + val expression = parseNumericExpressionWithAllErrors("sqrt(1+2)(3-7^2)(5+-17)") + + assertThat(expression).hasStructureThatMatches { + // sqrt(1+2)(3-7^2)(5+-17) -> (sqrt(1+2)*(3-7^2))*(5+-17) + multiplication(isImplicit = true) { leftOperand { - multiplication { + // sqrt(1+2)*(3-7^2) + multiplication(isImplicit = true) { leftOperand { + // sqrt(1+2) functionCallTo(SQUARE_ROOT) { argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } } } } } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + // (3-7^2) + group { + subtraction { + // 3 + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + // 7^2 + exponentiation { + leftOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } } } } @@ -1492,56 +1318,69 @@ class NumericExpressionParserTest { } } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + // (5+-17) + group { + addition { + leftOperand { + // 5 + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + rightOperand { + // -17 + negation { + operand { + // 17 + constant { + withValueThat().isIntegerThat().isEqualTo(17) + } + } + } } } } } } } + } - val expression53 = parseNumericExpressionWithAllErrors("√2√2√2") - assertThat(expression53).hasStructureThatMatches { - multiplication { - leftOperand { + @Test + fun testParse_multiplicationOfNegations_returnsExpWithCorrectStructure() { + val expression = parseNumericExpressionWithAllErrors("-2*-3") + + // Note that the following structure is not the same as (-2)*(-2) since unary negation has + // lower precedence than multiplication, so it's computed as first with its operand being the + // multiplication expression. + assertThat(expression).hasStructureThatMatches { + negation { + operand { multiplication { leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { + negation { + operand { constant { - withValueThat().isIntegerThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(3) } } } } } } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } } } + } - val expression54 = parseNumericExpressionWithAllErrors("2*2/-4+7*2") - assertThat(expression54).hasStructureThatMatches { + @Test + fun testParse_multipleOperations_withSubsequentBinaryAndUnaryOps_returnsExpWithCorrectOpOrder() { + val expression = parseNumericExpressionWithAllErrors("2*2/-4+7*2") + + assertThat(expression).hasStructureThatMatches { // 2*2/-4+7*2 -> ((2*2)/(-4))+(7*2) addition { leftOperand { @@ -1596,83 +1435,117 @@ class NumericExpressionParserTest { } } } + } - val expression55 = parseNumericExpressionWithAllErrors("3/(1-2)") - assertThat(expression55).hasStructureThatMatches { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } + @Test + fun testParse_operationsWithNonIntegersAndGroups_returnsExpWithCorrectOperationOrder() { + val expression = parseNumericExpressionWithAllErrors("7*(3.14/0.76+8.4)^(3.8+1/(2+2/(7.4+1)))") - val expression56 = parseNumericExpressionWithoutOptionalErrors("(3)/(1-2)") - assertThat(expression56).hasStructureThatMatches { - division { + assertThat(expression).hasStructureThatMatches { + // 7*(3.14/0.76+8.4)^(3.8+1/(2+2/(7.4+1))) -> 7*(((3.14/0.76)+8.4))^(3.8+(1/(2+(2/(7.4+1))))) + multiplication { leftOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) } } rightOperand { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + // (3.14/0.76)+8.4))^(3.8+(1/(2+(2/(7.4+1))) + exponentiation { + leftOperand { + // ((3.14/0.76)+8.4) + group { + addition { + leftOperand { + // 3.14/0.76 + division { + leftOperand { + // 3.14 + constant { + withValueThat().isIrrationalThat().isWithin(1e-5).of(3.14) + } + } + rightOperand { + // 0.76 + constant { + withValueThat().isIrrationalThat().isWithin(1e-5).of(0.76) + } + } + } + } + rightOperand { + // 8.4 + constant { + withValueThat().isIrrationalThat().isWithin(1e-5).of(8.4) + } + } } } } - } - } - } - } - - val expression57 = parseNumericExpressionWithoutOptionalErrors("3/((1-2))") - assertThat(expression57).hasStructureThatMatches { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - group { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) + rightOperand { + // (3.8+(1/(2+(2/(7.4+1))))) + group { + addition { + leftOperand { + // 3.8 + constant { + withValueThat().isIrrationalThat().isWithin(1e-5).of(3.8) + } } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + rightOperand { + // 1/(2+(2/(7.4+1))) + division { + leftOperand { + // 1 + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + // (2+(2/(7.4+1))) + group { + addition { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 2/(7.4+1) + division { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // (7.4+1) + group { + addition { + leftOperand { + // 7.4 + constant { + withValueThat().isIrrationalThat().isWithin(1e-5).of(7.4) + } + } + rightOperand { + // 1 + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + } + } + } + } + } + } + } } } } @@ -1681,35 +1554,33 @@ class NumericExpressionParserTest { } } } + } - // TODO: add others, including tests for malformed expressions throughout the parser & - // tokenizer. + @Test + fun testParse_twoSimilarExpressions_differedByWhitespace_areEqual() { + val expression1 = parseNumericExpressionWithAllErrors("3*2-3+4^3*8/3*2+7") + val expression2 = parseNumericExpressionWithAllErrors(" 3 * 2 - 3 + 4^ 3* 8/ 3 *2 + 7 ") + + // The parser should ensure that no positional information is kept (which result in the two + // expressions being equal). Note that not all expressions can be guaranteed to be exactly equal + // (particularly if they contain real values since rounding errors during parsing may cause + // inconsistencies). + assertThat(expression1).isEqualTo(expression2) } private companion object { - // TODO: fix helper API. - - private fun parseNumericExpressionWithoutOptionalErrors(expression: String): MathExpression { - val result = - parseNumericExpressionInternal( - expression, ErrorCheckingMode.REQUIRED_ONLY - ) - return (result as MathParsingResult.Success).result - } + private fun parseNumericExpressionWithoutOptionalErrors(expression: String): MathExpression = + parseNumericExpressionInternal(expression, ErrorCheckingMode.REQUIRED_ONLY) - private fun parseNumericExpressionWithAllErrors(expression: String): MathExpression { - val result = - parseNumericExpressionInternal( - expression, ErrorCheckingMode.ALL_ERRORS - ) - return (result as MathParsingResult.Success).result - } + private fun parseNumericExpressionWithAllErrors(expression: String): MathExpression = + parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) private fun parseNumericExpressionInternal( - expression: String, - errorCheckingMode: ErrorCheckingMode - ): MathParsingResult { - return MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) + expression: String, errorCheckingMode: ErrorCheckingMode + ): MathExpression { + val result = MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) + assertThat(result).isInstanceOf(MathParsingResult.Success::class.java) + return (result as MathParsingResult.Success).result } } } From 984e74019425da9e6161aa2b8ab777481c227c7d Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 21 Jan 2022 21:36:01 -0800 Subject: [PATCH 055/134] Finish algebraic expression tests. These largely rely on numeric expression tests (since they focus on verifying specific variable scenarios). --- .../math/AlgebraicExpressionParserTest.kt | 2158 +++++------------ 1 file changed, 620 insertions(+), 1538 deletions(-) diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt index 9a1957515bb..7950c52b336 100644 --- a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt @@ -11,388 +11,226 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingM import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode -/** Tests for [MathExpressionParser]. */ +/** + * Tests for [MathExpressionParser]. + * + * This test suite specifically focuses on verifying expressions that include variables. It largely + * assumes that core properties around precedence and associativity, and general handling of numeric + * expressions are correct through the tests managed by [NumericExpressionParserTest]. This is a + * valid approach since these tests are aware that an implementation is shared between the two. In + * the event the implementations are forked in the future, this test suite should correspondingly be + * updated to include tests for the core portions of parsing. For the most part, this suite assumes + * that its tests properly verify that variables can be a step-in replacement for numbers when + * considering numeric expression tests (with some special exceptions around implicit multiplication + * to enable polynomial syntax). + * + * This suite does not test errors--see [MathExpressionParserTest] for those tests. Further, it does + * not test algebraic equations (see [AlgebraicEquationParserTest] for those tests). + * + * This test suite largely focuses on demonstrating polynomial syntax since that's the expected + * principal use of algebraic expressions and equations. + */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class AlgebraicExpressionParserTest { - // TODO: finish docs. - @Test - fun testLotsOfCasesForAlgebraicExpression() { - // TODO: split this up - // TODO: add log string generation for expressions. - val expression1 = parseAlgebraicExpressionWithAllErrors("1") - assertThat(expression1).hasStructureThatMatches { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } + fun testParse_variable_returnsExpWithVariable() { + val expression = parseAlgebraicExpressionWithAllErrors("x") - val expression61 = parseAlgebraicExpressionWithAllErrors("x") - assertThat(expression61).hasStructureThatMatches { + assertThat(expression).hasStructureThatMatches { variable { withNameThat().isEqualTo("x") } } + } - val expression2 = parseAlgebraicExpressionWithAllErrors(" 2 ") - assertThat(expression2).hasStructureThatMatches { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - - val expression3 = parseAlgebraicExpressionWithAllErrors(" 2.5 ") - assertThat(expression3).hasStructureThatMatches { - constant { - withValueThat().isIrrationalThat().isWithin(1e-5).of(2.5) - } - } - - val expression62 = parseAlgebraicExpressionWithAllErrors(" y ") - assertThat(expression62).hasStructureThatMatches { - variable { - withNameThat().isEqualTo("y") - } - } + @Test + fun testParse_variablePlusInteger_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("x+1") - val expression63 = parseAlgebraicExpressionWithAllErrors(" z x ") - assertThat(expression63).hasStructureThatMatches { - multiplication { + assertThat(expression).hasStructureThatMatches { + addition { leftOperand { variable { - withNameThat().isEqualTo("z") + withNameThat().isEqualTo("x") } } rightOperand { - variable { - withNameThat().isEqualTo("x") + constant { + withValueThat().isIntegerThat().isEqualTo(1) } } } } + } - val expression4 = parseAlgebraicExpressionWithoutOptionalErrors("2^3^2") - assertThat(expression4).hasStructureThatMatches { - exponentiation { + @Test + fun testParse_integerPlusVariable_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("1+x") + + assertThat(expression).hasStructureThatMatches { + addition { leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + variable { + withNameThat().isEqualTo("x") } } } } + } - val expression23 = parseAlgebraicExpressionWithAllErrors("(2^3)^2") - assertThat(expression23).hasStructureThatMatches { - exponentiation { + @Test + fun testParse_variableMinusInteger_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("x-1") + + assertThat(expression).hasStructureThatMatches { + subtraction { leftOperand { - group { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } + variable { + withNameThat().isEqualTo("x") } } rightOperand { constant { - withValueThat().isIntegerThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(1) } } } } + } - val expression24 = parseAlgebraicExpressionWithAllErrors("512/32/4") - assertThat(expression24).hasStructureThatMatches { - division { + @Test + fun testParse_variableMinusInteger_mathSymbol_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("x−1") + + assertThat(expression).hasStructureThatMatches { + subtraction { leftOperand { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(512) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(32) - } - } + variable { + withNameThat().isEqualTo("x") } } rightOperand { constant { - withValueThat().isIntegerThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(1) } } } } + } - val expression25 = parseAlgebraicExpressionWithAllErrors("512/(32/4)") - assertThat(expression25).hasStructureThatMatches { - division { + @Test + fun testParse_integerMinusVariable_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("1-x") + + assertThat(expression).hasStructureThatMatches { + subtraction { leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(512) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { - group { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(32) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } + variable { + withNameThat().isEqualTo("x") } } } } + } - val expression5 = parseAlgebraicExpressionWithAllErrors("sqrt(2)") - assertThat(expression5).hasStructureThatMatches { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } + @Test + fun testParse_integerMinusVariable_mathSymbol_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("1−x") - val expression64 = parseAlgebraicExpressionWithoutOptionalErrors("xyz(2)") - assertThat(expression64).hasStructureThatMatches { - multiplication { + assertThat(expression).hasStructureThatMatches { + subtraction { leftOperand { - multiplication { - leftOperand { - multiplication { - leftOperand { - variable { - withNameThat().isEqualTo("x") - } - } - rightOperand { - variable { - withNameThat().isEqualTo("y") - } - } - } - } - rightOperand { - variable { - withNameThat().isEqualTo("z") - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } + variable { + withNameThat().isEqualTo("x") } } } } + } - val expression6 = parseAlgebraicExpressionWithAllErrors("732") - assertThat(expression6).hasStructureThatMatches { - constant { - withValueThat().isIntegerThat().isEqualTo(732) - } - } + @Test + fun testParse_variableTimesInteger_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("x*2") - // Verify order of operations between higher & lower precedent operators. - val expression32 = parseAlgebraicExpressionWithAllErrors("3+4^5") - assertThat(expression32).hasStructureThatMatches { - addition { + assertThat(expression).hasStructureThatMatches { + multiplication { leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) + variable { + withNameThat().isEqualTo("x") } } rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } } } + } - val expression7 = parseAlgebraicExpressionWithoutOptionalErrors("3*2-3+4^7*8/3*2+7") - assertThat(expression7).hasStructureThatMatches { - // To better visualize the precedence & order of operations, see this grouped version: - // (((3*2)-3)+((((4^7)*8)/3)*2))+7. - addition { + @Test + fun testParse_variableTimesInteger_mathSymbol_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("x×2") + + assertThat(expression).hasStructureThatMatches { + multiplication { leftOperand { - // ((3*2)-3)+((((4^7)*8)/3)*2) - addition { - leftOperand { - // (1*2)-3 - subtraction { - leftOperand { - // 3*2 - multiplication { - leftOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - rightOperand { - // (((4^7)*8)/3)*2 - multiplication { - leftOperand { - // ((4^7)*8)/3 - division { - leftOperand { - // (4^7)*8 - multiplication { - leftOperand { - // 4^7 - exponentiation { - leftOperand { - // 4 - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - rightOperand { - // 7 - constant { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - } - } - rightOperand { - // 8 - constant { - withValueThat().isIntegerThat().isEqualTo(8) - } - } - } - } - rightOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - rightOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } + variable { + withNameThat().isEqualTo("x") } } rightOperand { - // 7 constant { - withValueThat().isIntegerThat().isEqualTo(7) + withValueThat().isIntegerThat().isEqualTo(2) } } } } + } + + @Test + fun testParse_integerTimesVariable_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("2*x") - val expression8 = parseAlgebraicExpressionWithAllErrors("(1+2)(3+4)") - assertThat(expression8).hasStructureThatMatches { + assertThat(expression).hasStructureThatMatches { multiplication { leftOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } + variable { + withNameThat().isEqualTo("x") } } } } + } - val expression10 = parseAlgebraicExpressionWithAllErrors("2(1+2)") - assertThat(expression10).hasStructureThatMatches { + @Test + fun testParse_integerTimesVariable_mathSymbol_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("2×x") + + assertThat(expression).hasStructureThatMatches { multiplication { leftOperand { constant { @@ -400,176 +238,189 @@ class AlgebraicExpressionParserTest { } } rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } + variable { + withNameThat().isEqualTo("x") } } } } + } - val expression12 = parseAlgebraicExpressionWithAllErrors("3sqrt(2)") - assertThat(expression12).hasStructureThatMatches { - multiplication { + @Test + fun testParse_variableDividedByInteger_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("x/2") + + assertThat(expression).hasStructureThatMatches { + division { leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { constant { - withValueThat().isIntegerThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + @Test + fun testParse_variableDividedByInteger_mathSymbol_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("x÷2") + + assertThat(expression).hasStructureThatMatches { + division { + leftOperand { + variable { + withNameThat().isEqualTo("x") } } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } } } + } - val expression65 = parseAlgebraicExpressionWithAllErrors("xsqrt(2)") - assertThat(expression65).hasStructureThatMatches { - multiplication { + @Test + fun testParse_integerDividedByVariable_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("1/x") + + assertThat(expression).hasStructureThatMatches { + division { leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { variable { withNameThat().isEqualTo("x") } } + } + } + } + + @Test + fun testParse_integerDividedByVariable_mathSymbol_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("1÷x") + + assertThat(expression).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + variable { + withNameThat().isEqualTo("x") } } } } + } - val expression13 = parseAlgebraicExpressionWithAllErrors("sqrt(2)*(1+2)*(3-2^5)") - assertThat(expression13).hasStructureThatMatches { - multiplication { + @Test + fun testParse_variableRaisedToInteger_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("x^2") + + // This demonstrates basic quadratic polynomial syntax. + assertThat(expression).hasStructureThatMatches { + exponentiation { leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } + variable { + withNameThat().isEqualTo("x") } } rightOperand { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - } - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + @Test + fun testParse_intRaisedToVariable_noOptionalErrors_returnsExpWithVariableBasedBinaryOperation() { + // Note that optional errors prohibit variables in exponents. + val expression = parseAlgebraicExpressionWithoutOptionalErrors("2^x") + + assertThat(expression).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + } + + @Test + fun testParse_negatedVariable_returnsExpWithVariableBasedUnaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("-x") + + assertThat(expression).hasStructureThatMatches { + negation { + operand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + } + + @Test + fun testParse_positiveVariable_withoutOptionalErrors_returnsExpWithVariableBasedUnaryOperation() { + // Note that optional errors prohibit unary positive operations. + val expression = parseAlgebraicExpressionWithoutOptionalErrors("+x") + + assertThat(expression).hasStructureThatMatches { + positive { + operand { + variable { + withNameThat().isEqualTo("x") } } } } + } + + @Test + fun testParse_variableInGroup_returnsExpWithVariableInGroup() { + val expression = parseAlgebraicExpressionWithAllErrors("2*(1+x)") - val expression58 = parseAlgebraicExpressionWithAllErrors("sqrt(2)(1+2)(3-2^5)") - assertThat(expression58).hasStructureThatMatches { + assertThat(expression).hasStructureThatMatches { multiplication { leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { group { - subtraction { + addition { leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } + variable { + withNameThat().isEqualTo("x") } } } @@ -577,1125 +428,360 @@ class AlgebraicExpressionParserTest { } } } + } - val expression14 = parseAlgebraicExpressionWithoutOptionalErrors("((3))") - assertThat(expression14).hasStructureThatMatches { - group { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) + @Test + fun testParse_inlineSquareRootOfX_returnsExpWithVariableInFunctionArgument() { + val expression = parseAlgebraicExpressionWithAllErrors("√x") + + assertThat(expression).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + variable { + withNameThat().isEqualTo("x") } } } } + } - val expression15 = parseAlgebraicExpressionWithoutOptionalErrors("++3") - assertThat(expression15).hasStructureThatMatches { - positive { - operand { - positive { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } + @Test + fun testParse_squareRootOfX_returnsExpWithVariableInFunctionArgument() { + val expression = parseAlgebraicExpressionWithAllErrors("sqrt(x)") + + assertThat(expression).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + } + + @Test + fun testParse_integerAndVariable_returnsExpWithImplicitMultiplication() { + val expression = parseAlgebraicExpressionWithAllErrors("2x") + + // This also demonstrates basic polynomial syntax for linear equations. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") } } } } + } + + @Test + fun testParse_negatedIntegerAndVariable_returnsExpWithImplicitMultiplicationWithUnary() { + val expression = parseAlgebraicExpressionWithAllErrors("-2x") - val expression16 = parseAlgebraicExpressionWithoutOptionalErrors("--4") - assertThat(expression16).hasStructureThatMatches { + // Similar to the previous test, but this ensures negation ordering (relative to variables & + // implicit multiplication). + assertThat(expression).hasStructureThatMatches { negation { operand { - negation { - operand { + multiplication(isImplicit = true) { + leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") } } } } } } + } - val expression17 = parseAlgebraicExpressionWithAllErrors("1+-4") - assertThat(expression17).hasStructureThatMatches { - addition { + @Test + fun testParse_xInlineSquareRootOfInteger_returnsExpWithImplicitMultiplication() { + val expression = parseAlgebraicExpressionWithAllErrors("x√2") + + // A variable next to a square root indicates implicit multiplication. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) + variable { + withNameThat().isEqualTo("x") } } rightOperand { - negation { - operand { + functionCallTo(SQUARE_ROOT) { + argument { constant { - withValueThat().isIntegerThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(2) } } } } } } + } - val expression18 = parseAlgebraicExpressionWithoutOptionalErrors("1++4") - assertThat(expression18).hasStructureThatMatches { - addition { + @Test + fun testParse_xSquareRootOfInteger_returnsExpWithImplicitMultiplication() { + val expression = parseAlgebraicExpressionWithAllErrors("xsqrt(2)") + + // Even with 'sqrt' and x being tightly together, the parser still sees a distinct variable and + // function. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) + variable { + withNameThat().isEqualTo("x") } } rightOperand { - positive { - operand { + functionCallTo(SQUARE_ROOT) { + argument { constant { - withValueThat().isIntegerThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(2) } } } } } } + } - val expression19 = parseAlgebraicExpressionWithAllErrors("1--4") - assertThat(expression19).hasStructureThatMatches { - subtraction { + @Test + fun testParse_variableTimesVariable_same_returnsExpWithMultiedVariables() { + val expression = parseAlgebraicExpressionWithAllErrors("x*x") + + assertThat(expression).hasStructureThatMatches { + multiplication { leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) + variable { + withNameThat().isEqualTo("x") } } rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } + variable { + withNameThat().isEqualTo("x") } } } } + } - val expression20 = parseAlgebraicExpressionWithAllErrors("√2 × 7 ÷ 4") - assertThat(expression20).hasStructureThatMatches { - division { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - - val expression21 = parseAlgebraicExpressionWithAllErrors("sqrt(2)sqrt(3)sqrt(4)") - // Note that this tree demonstrates left associativity. - assertThat(expression21).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - - val expression22 = parseAlgebraicExpressionWithAllErrors("(1+2)(3-7^2)(5+-17)") - assertThat(expression22).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - // 1+2 - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - rightOperand { - // 3-7^2 - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - } - } - rightOperand { - // 5+-17 - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(17) - } - } - } - } - } - } - } - } - } - - val expression26 = parseAlgebraicExpressionWithAllErrors("3^-2") - assertThat(expression26).hasStructureThatMatches { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - - val expression27 = parseAlgebraicExpressionWithoutOptionalErrors("(3^-2)^(3^-2)") - assertThat(expression27).hasStructureThatMatches { - exponentiation { - leftOperand { - group { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - rightOperand { - group { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - } - } - - val expression28 = parseAlgebraicExpressionWithAllErrors("1-3^sqrt(4)") - assertThat(expression28).hasStructureThatMatches { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - } - - // "Hard" order of operation problems loosely based on & other problems that can often stump - // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. - val expression29 = parseAlgebraicExpressionWithAllErrors("3÷2*(3+4)") - assertThat(expression29).hasStructureThatMatches { - multiplication { - leftOperand { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - - val expression59 = parseAlgebraicExpressionWithAllErrors("3÷2(3+4)") - assertThat(expression59).hasStructureThatMatches { - multiplication { - leftOperand { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - - val expression31 = parseAlgebraicExpressionWithoutOptionalErrors("(3)(4)(5)") - assertThat(expression31).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - } - } - } - - val expression33 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)") - assertThat(expression33).hasStructureThatMatches { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - - // Verify that implicit multiple has lower precedence than exponentiation. - val expression34 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)(4)") - assertThat(expression34).hasStructureThatMatches { - multiplication { - leftOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - - val expression35 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)*2^2") - assertThat(expression35).hasStructureThatMatches { - multiplication { - leftOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - - // An exponentiation can be a right operand of an implicit mult if it's grouped. - val expression36 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)(2^2)") - assertThat(expression36).hasStructureThatMatches { - multiplication { - leftOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - rightOperand { - group { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - - val expression38 = parseAlgebraicExpressionWithoutOptionalErrors("2^3(4)*2^3") - assertThat(expression38).hasStructureThatMatches { - // 2^3(4)*2^3 - multiplication { - leftOperand { - // 2^3(4) - multiplication { - leftOperand { - // 2^3 - exponentiation { - leftOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - rightOperand { - // 4 - group { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - rightOperand { - // 2^3 - exponentiation { - leftOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - } - - val expression39 = parseAlgebraicExpressionWithAllErrors("-(1+2)") - assertThat(expression39).hasStructureThatMatches { - negation { - operand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - - // Should pass for algebra. - val expression66 = parseAlgebraicExpressionWithAllErrors("-2 x") - assertThat(expression66).hasStructureThatMatches { - negation { - operand { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - variable { - withNameThat().isEqualTo("x") - } - } - } - } - } - } - - val expression40 = parseAlgebraicExpressionWithAllErrors("-2 (1+2)") - assertThat(expression40).hasStructureThatMatches { - // The negation happens last for parity with other common calculators. - negation { - operand { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - } - } - - val expression41 = parseAlgebraicExpressionWithoutOptionalErrors("-2^3(4)") - assertThat(expression41).hasStructureThatMatches { - negation { - operand { - multiplication { - leftOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - - val expression43 = parseAlgebraicExpressionWithoutOptionalErrors("√2^2(3)") - assertThat(expression43).hasStructureThatMatches { - multiplication { - leftOperand { - exponentiation { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - - val expression60 = parseAlgebraicExpressionWithoutOptionalErrors("√(2^2(3))") - assertThat(expression60).hasStructureThatMatches { - functionCallTo(SQUARE_ROOT) { - argument { - group { - multiplication { - leftOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - } - } - } - - val expression42 = parseAlgebraicExpressionWithAllErrors("-2*-2") - // Note that the following structure is not the same as (-2)*(-2) since unary negation has - // higher precedence than multiplication, so it's first & recurses to include the entire - // multiplication expression. - assertThat(expression42).hasStructureThatMatches { - negation { - operand { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - } - - val expression44 = parseAlgebraicExpressionWithoutOptionalErrors("2(2)") - assertThat(expression44).hasStructureThatMatches { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - - val expression45 = parseAlgebraicExpressionWithAllErrors("2sqrt(2)") - assertThat(expression45).hasStructureThatMatches { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - - val expression46 = parseAlgebraicExpressionWithAllErrors("2√2") - assertThat(expression46).hasStructureThatMatches { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - - val expression47 = parseAlgebraicExpressionWithoutOptionalErrors("(2)(2)") - assertThat(expression47).hasStructureThatMatches { - multiplication { - leftOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } + @Test + fun testParse_variableTimesVariable_returnsExpWithMultipleVariables() { + val expression = parseAlgebraicExpressionWithAllErrors("x*y") - val expression48 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)(2)") - assertThat(expression48).hasStructureThatMatches { + assertThat(expression).hasStructureThatMatches { multiplication { leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + variable { + withNameThat().isEqualTo("x") } } rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } + variable { + withNameThat().isEqualTo("y") } } } } + } - val expression49 = parseAlgebraicExpressionWithAllErrors("sqrt(2)sqrt(2)") - assertThat(expression49).hasStructureThatMatches { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } + @Test + fun testParse_variableNextToVariable_returnsExpWithImplicitMultiplication() { + val expression = parseAlgebraicExpressionWithAllErrors("xy") - val expression50 = parseAlgebraicExpressionWithAllErrors("√2√2") - assertThat(expression50).hasStructureThatMatches { - multiplication { + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + variable { + withNameThat().isEqualTo("x") } } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + variable { + withNameThat().isEqualTo("y") } } } } + } - val expression51 = parseAlgebraicExpressionWithoutOptionalErrors("(2)(2)(2)") - assertThat(expression51).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } + @Test + fun testParse_threeSubsequentVariables_returnsExpWithImplicitMultAndLeftAssociativity() { + val expression = parseAlgebraicExpressionWithAllErrors("xyz") - val expression52 = parseAlgebraicExpressionWithAllErrors("sqrt(2)sqrt(2)sqrt(2)") - assertThat(expression52).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + // 'xyz' results in a right-associative implicit multiplication (i.e. (x*y)*z). + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { + leftOperand { + multiplication(isImplicit = true) { + leftOperand { + variable { + withNameThat().isEqualTo("x") } } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + variable { + withNameThat().isEqualTo("y") } } } } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + variable { + withNameThat().isEqualTo("z") } } } } + } - val expression53 = parseAlgebraicExpressionWithAllErrors("√2√2√2") - assertThat(expression53).hasStructureThatMatches { - multiplication { + @Test + fun testParse_threeSubsequentVariables_customVariables_returnsExpWithImplicitMult() { + val allowedVariables = listOf("i", "j", "k") + + val expression = parseAlgebraicExpressionWithAllErrors("ijk", allowedVariables) + + // Other variables can be used, too. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { leftOperand { - multiplication { + multiplication(isImplicit = true) { leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + variable { + withNameThat().isEqualTo("i") } } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + variable { + withNameThat().isEqualTo("j") } } } } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + variable { + withNameThat().isEqualTo("k") } } } } + } - // Should pass for algebra. - val expression67 = parseAlgebraicExpressionWithAllErrors("2x^2y^-3") - assertThat(expression67).hasStructureThatMatches { - // 2x^2y^-3 -> (2*(x^2))*(y^(-3)) - multiplication { - // 2x^2 + @Test + fun testParse_fullQuadraticExpression_returnsExpWithCorrectValuesAndOrders() { + val expression = parseAlgebraicExpressionWithAllErrors("-8x^3-7.4x^2+x-12/√2") + + // This combines all of the distinct pieces tested earlier to demonstrate full polynomial + // syntax. + assertThat(expression).hasStructureThatMatches { + // -8x^3-7.4x^2+x-12√2 -> (((-(8*(x^3))) - (7.4*(x^2))) + x) - (12/√2) + subtraction { leftOperand { - multiplication { - // 2 + // ((-(8*(x^3))) - (7.4*(x^2))) + x + addition { leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - // x^2 - rightOperand { - exponentiation { - // x + // (-(8*(x^3))) - (7.4*(x^2)) + subtraction { leftOperand { - variable { - withNameThat().isEqualTo("x") + // -(8*(x^3)) + negation { + operand { + // 8*(x^3) + multiplication(isImplicit = true) { + leftOperand { + // 8 + constant { + withValueThat().isIntegerThat().isEqualTo(8) + } + } + rightOperand { + // x^3 + exponentiation { + leftOperand { + // x + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } } } - // 2 rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + // 7.4*(x^2) + multiplication(isImplicit = true) { + leftOperand { + // 7.4 + constant { + withValueThat().isIrrationalThat().isWithin(1e-5).of(7.4) + } + } + rightOperand { + // x^2 + exponentiation { + leftOperand { + // x + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } } } } } + rightOperand { + // x + variable { + withNameThat().isEqualTo("x") + } + } } } - // y^-3 rightOperand { - exponentiation { - // y + // 12/√2 + division { leftOperand { - variable { - withNameThat().isEqualTo("y") + // 12 + constant { + withValueThat().isIntegerThat().isEqualTo(12) } } - // -3 rightOperand { - negation { - // 3 - operand { + // √2 + functionCallTo(SQUARE_ROOT) { + argument { + // 2 constant { - withValueThat().isIntegerThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1704,38 +790,130 @@ class AlgebraicExpressionParserTest { } } } + } - val expression54 = parseAlgebraicExpressionWithAllErrors("2*2/-4+7*2") - assertThat(expression54).hasStructureThatMatches { - // 2*2/-4+7*2 -> ((2*2)/(-4))+(7*2) - addition { + @Test + fun testParse_expressionWithMultipleVariableQuadraticTerms_returnsExpWithCorrectValsAndOrders() { + val expression = parseAlgebraicExpressionWithAllErrors("12x^2y^2-yz^2+yzx-731z") + + // This builds on the polynomial syntax demonstrated above by demonstrating multi-variable + // implicit multiplication for multi-dimensional polynomials. Note that this also demonstrates + // that exponents can imply multiplication when the base is a variable (as part of polynomial + // syntax) which is a functional difference from numeric-only expressions (where subsequent + // numeric exponents never imply multiplication). + assertThat(expression).hasStructureThatMatches { + // 12x^2y^2-yz^2+yzx-731z -> ((((12*(x^2))*(y^2))-(y*(z^2)))+((y*z)*x))-(731*z) + subtraction { leftOperand { - // 2*2/-4 - division { + // (((12*(x^2))*(y^2))-(y*(z^2)))+((y*z)*x) + addition { leftOperand { - // 2*2 - multiplication { + // ((12*(x^2))*(y^2))-(y*(z^2)) + subtraction { leftOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) + // (12*(x^2))*(y^2) + multiplication(isImplicit = true) { + leftOperand { + // 12*(x^2) + multiplication(isImplicit = true) { + leftOperand { + // 12 + constant { + withValueThat().isIntegerThat().isEqualTo(12) + } + } + rightOperand { + // x^2 + exponentiation { + leftOperand { + // x + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + // y^2 + exponentiation { + leftOperand { + // y + variable { + withNameThat().isEqualTo("y") + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } } } rightOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) + // y*(z^2) + multiplication(isImplicit = true) { + leftOperand { + // y + variable { + withNameThat().isEqualTo("y") + } + } + rightOperand { + // z^2 + exponentiation { + leftOperand { + // z + variable { + withNameThat().isEqualTo("z") + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } } } } } rightOperand { - // -4 - negation { - // 4 - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) + // (y*z)*x + multiplication(isImplicit = true) { + leftOperand { + // y*z + multiplication(isImplicit = true) { + leftOperand { + // y + variable { + withNameThat().isEqualTo("y") + } + } + rightOperand { + // z + variable { + withNameThat().isEqualTo("z") + } + } + } + } + rightOperand { + // x + variable { + withNameThat().isEqualTo("x") } } } @@ -1743,147 +921,51 @@ class AlgebraicExpressionParserTest { } } rightOperand { - // 7*2 - multiplication { + // 731*z + multiplication(isImplicit = true) { leftOperand { - // 7 + // 731 constant { - withValueThat().isIntegerThat().isEqualTo(7) + withValueThat().isIntegerThat().isEqualTo(731) } } rightOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - - val expression55 = parseAlgebraicExpressionWithAllErrors("3/(1-2)") - assertThat(expression55).hasStructureThatMatches { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - - val expression56 = parseAlgebraicExpressionWithoutOptionalErrors("(3)/(1-2)") - assertThat(expression56).hasStructureThatMatches { - division { - leftOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - rightOperand { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - - val expression57 = parseAlgebraicExpressionWithoutOptionalErrors("3/((1-2))") - assertThat(expression57).hasStructureThatMatches { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - group { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + // z + variable { + withNameThat().isEqualTo("z") } } } } } } - - // TODO: add others, including tests for malformed expressions throughout the parser & - // tokenizer. } private companion object { - // TODO: fix helper API. - private fun parseAlgebraicExpressionWithoutOptionalErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") + expression: String, allowedVariables: List = listOf("x", "y", "z") ): MathExpression { - val result = - parseAlgebraicExpressionInternal( - expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables - ) - return (result as MathParsingResult.Success).result + return parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables + ) } private fun parseAlgebraicExpressionWithAllErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") + expression: String, allowedVariables: List = listOf("x", "y", "z") ): MathExpression { - val result = - parseAlgebraicExpressionInternal( - expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables - ) - return (result as MathParsingResult.Success).result + return parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables + ) } private fun parseAlgebraicExpressionInternal( - expression: String, - errorCheckingMode: ErrorCheckingMode, - allowedVariables: List = listOf("x", "y", "z") - ): MathParsingResult { - return MathExpressionParser.parseAlgebraicExpression( + expression: String, errorCheckingMode: ErrorCheckingMode, allowedVariables: List + ): MathExpression { + val result = MathExpressionParser.parseAlgebraicExpression( expression, allowedVariables, errorCheckingMode ) + assertThat(result).isInstanceOf(MathParsingResult.Success::class.java) + return (result as MathParsingResult.Success).result } } } From 042a1adfbbe80daaebb950950bc8f91c2ad48883 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 24 Jan 2022 15:21:15 -0800 Subject: [PATCH 056/134] Add missing tests for better coverage. --- .../android/util/math/MathExpressionParser.kt | 22 +++++---------- .../util/math/MathExpressionParserTest.kt | 27 ++++++++++++++++++- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt index e0afccf30e4..25a40605beb 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -303,13 +303,8 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo parseContext.hasNextTokenOfType() || parseContext.hasNextTokenOfType() } ?: SpacesBetweenNumbersError.toFailure() - is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol -> + is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol, is VariableName -> parseGenericTermWithoutUnaryWithoutNumber() - is VariableName -> { - if (parseContext is AlgebraicExpressionContext) { - parseGenericTermWithoutUnaryWithoutNumber() - } else VariableInNumericExpressionError.toFailure() - } is DivideSymbol, is ExponentiationSymbol, is MultiplySymbol -> { val previousToken = parseContext.getPreviousToken() when { @@ -528,9 +523,7 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo private fun parseVariable(): MathParsingResult { val variableNameResult = - parseContext.consumeTokenOfType().maybeFail { - if (!parseContext.allowsVariables()) GenericError else null - }.maybeFail { variableName -> + parseContext.consumeTokenOfType().maybeFail { variableName -> return@maybeFail if (parseContext.hasMoreTokens()) { when (val nextToken = parseContext.peekToken()) { is PositiveInteger -> @@ -654,9 +647,12 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } is IncompleteFunctionName -> nextToken.toError() is InvalidToken -> nextToken.toError() + is VariableName -> if (parseContext !is AlgebraicExpressionContext) { + VariableInNumericExpressionError + } else GenericError is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is ExponentiationSymbol, is FunctionName, is MinusSymbol, is MultiplySymbol, is PlusSymbol, is SquareRootSymbol, - is VariableName, null -> GenericError + null -> GenericError } } else null } @@ -687,8 +683,6 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo abstract val errorCheckingMode: ErrorCheckingMode - abstract fun allowsVariables(): Boolean - fun hasMoreTokens(): Boolean = tokens.hasNext() fun peekToken(): Token? = tokens.peek() @@ -725,8 +719,6 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo rawExpression: String, override val errorCheckingMode: ErrorCheckingMode ) : ParseContext(rawExpression) { - // Numeric expressions never allow variables. - override fun allowsVariables(): Boolean = false } class AlgebraicExpressionContext( @@ -736,8 +728,6 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo override val errorCheckingMode: ErrorCheckingMode ) : ParseContext(rawExpression) { fun allowsVariable(variableName: String): Boolean = variableName in allowedVariables - - override fun allowsVariables(): Boolean = true } } diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt index 4bfdb4199da..5a37e71a372 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -737,7 +737,8 @@ class MathExpressionParserTest { Iteration("var_directly_in_exp", "subExp=x"), Iteration("var_directly_in_sub_exp", "subExp=(1+x)"), Iteration("var_directly_in_nested_exp", "subExp=3^x"), - Iteration("var_directly_in_sqrt", "subExp=sqrt(x)") + Iteration("var_directly_in_sqrt", "subExp=sqrt(x)"), + Iteration("var_in_unary", "subExp=-x") ) fun testParseAlgExp_powersWithVariableExpressions_returnsExponentIsVariableExpressionError() { val expression = "2^$subExp" @@ -794,6 +795,14 @@ class MathExpressionParserTest { assertThat(error).isNestedExponents() } + @Test + fun testParseNumExp_nestedExponents_withUnary_returnsNestedExponentsError() { + val error = expectFailureWhenParsingAlgebraicExpression("2^-3^4") + + // This covers a slightly different case than the above. + assertThat(error).isNestedExponents() + } + @Test fun testParseAlgExp_nestedExponents_optionalErrorsDisabled_doesNotFail() { // This doesn't trigger a failure when optional errors are disabled. @@ -841,6 +850,14 @@ class MathExpressionParserTest { assertThat(error).isVariableInNumericExpression() } + @Test + fun testParseNumExp_addVariable_afterNumber_returnsVariableInNumericExpressionError() { + val error = expectFailureWhenParsingNumericExpression("2x") + + // This covers a slightly different case than the above. + assertThat(error).isVariableInNumericExpression() + } + @Test fun testParseAlgExp_addUnsupportedVariable_returnsDisabledVariablesInUseErrorWithDetails() { val allowedVariables = listOf("x", "y") @@ -1050,6 +1067,14 @@ class MathExpressionParserTest { assertThat(error).isGenericError() } + @Test + fun testParseNumExp_trailingEquals_returnsGenericError() { + val error = expectFailureWhenParsingNumericExpression("+=") + + // Expressions can't end with an equals sign. + assertThat(error).isGenericError() + } + private companion object { private val BINARY_SYMBOL_TO_OPERATOR_MAP = mapOf( "*" to MULTIPLY, From 8ead9bf822b7b73fe511000b3e18b5b445ee6fa7 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 24 Jan 2022 17:49:23 -0800 Subject: [PATCH 057/134] Add KDocs & test exemptions. --- scripts/assets/test_file_exemptions.textproto | 2 + .../testing/math/MathParsingErrorSubject.kt | 219 +++++++++++++++++- .../android/util/math/MathExpressionParser.kt | 123 +++++++++- .../android/util/math/MathParsingError.kt | 169 ++++++++++++++ 4 files changed, 493 insertions(+), 20 deletions(-) diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index e7b076af277..f1715400516 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -658,6 +658,7 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/Fracti exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/TokenSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/mockito/MockitoKotlinHelper.kt" @@ -724,6 +725,7 @@ 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/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/math/MathParsingError.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/testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt index 5db1b5f61d9..53c8bad235f 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt @@ -9,6 +9,13 @@ import com.google.common.truth.Truth.assertAbout import com.google.common.truth.Truth.assertThat import org.oppia.android.app.model.MathBinaryOperation import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat +import org.oppia.android.testing.math.MathParsingErrorSubject.Companion.assertThat +import org.oppia.android.testing.math.MathParsingErrorSubject.MultipleRedundantParenthesesSubject.Companion.assertThat +import org.oppia.android.testing.math.MathParsingErrorSubject.NoVariableOrNumberAfterBinaryOperatorSubject.Companion.assertThat +import org.oppia.android.testing.math.MathParsingErrorSubject.NoVariableOrNumberBeforeBinaryOperatorSubject.Companion.assertThat +import org.oppia.android.testing.math.MathParsingErrorSubject.NumberAfterVariableSubject.Companion.assertThat +import org.oppia.android.testing.math.MathParsingErrorSubject.RedundantParenthesesForIndividualTermsSubject.Companion.assertThat +import org.oppia.android.testing.math.MathParsingErrorSubject.SubsequentBinaryOperatorsSubject.Companion.assertThat import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.oppia.android.util.math.MathParsingError import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError @@ -36,104 +43,168 @@ import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError -// TODO: file issue to add tests. +// TODO(#4132): file issue to add tests. -class MathParsingErrorSubject( +/** + * Truth subject for verifying properties of [MathParsingError]s. + * + * Call [assertThat] to create the subject. + */ +class MathParsingErrorSubject private constructor( metadata: FailureMetadata, private val actual: MathParsingError ) : Subject(metadata, actual) { + /** Verifies that the [MathParsingError] being tested is a [SpacesBetweenNumbersError]. */ fun isSpacesBetweenNumbers() { assertThat(actual).isEqualTo(SpacesBetweenNumbersError) } + /** Verifies that the [MathParsingError] being tested is an [UnbalancedParenthesesError]. */ fun isUnbalancedParentheses() { assertThat(actual).isEqualTo(UnbalancedParenthesesError) } + /** + * Verifies that the [MathParsingError] being tested is a [SingleRedundantParenthesesError], and + * returns a [SingleRedundantParenthesesSubject] to test its specific attributes. + */ fun isSingleRedundantParenthesesThat(): SingleRedundantParenthesesSubject { return SingleRedundantParenthesesSubject.assertThat(verifyAsType()) } + /** + * Verifies that the [MathParsingError] being tested is a [MultipleRedundantParenthesesError], and + * returns a [MultipleRedundantParenthesesSubject] to test its specific attributes. + */ fun isMultipleRedundantParenthesesThat(): MultipleRedundantParenthesesSubject { return MultipleRedundantParenthesesSubject.assertThat(verifyAsType()) } + /** + * Verifies that the [MathParsingError] being tested is a + * [RedundantParenthesesForIndividualTermsError], and returns a + * [RedundantParenthesesForIndividualTermsSubject] to test its specific attributes. + */ fun isRedundantIndividualTermsParensThat(): RedundantParenthesesForIndividualTermsSubject { return RedundantParenthesesForIndividualTermsSubject.assertThat(verifyAsType()) } + /** + * Verifies that the [MathParsingError] being tested is an [UnnecessarySymbolsError], and returns + * a [StringSubject] to verify the symbol's specific value. + */ fun isUnnecessarySymbolWithSymbolThat(): StringSubject { return assertThat(verifyAsType().invalidSymbol) } + /** + * Verifies that the [MathParsingError] being tested is a [NumberAfterVariableError], and returns + * a [NumberAfterVariableSubject] to test its specific attributes. + */ fun isNumberAfterVariableThat(): NumberAfterVariableSubject { return NumberAfterVariableSubject.assertThat(verifyAsType()) } + /** + * Verifies that the [MathParsingError] being tested is a [SubsequentBinaryOperatorsError], and + * returns a [SubsequentBinaryOperatorsSubject] to test its specific attributes. + */ fun isSubsequentBinaryOperatorsThat(): SubsequentBinaryOperatorsSubject { return SubsequentBinaryOperatorsSubject.assertThat(verifyAsType()) } + /** Verifies that the [MathParsingError] being tested is a [SubsequentUnaryOperatorsError]. */ fun isSubsequentUnaryOperators() { assertThat(actual).isEqualTo(SubsequentUnaryOperatorsError) } + /** + * Verifies that the [MathParsingError] being tested is a + * [NoVariableOrNumberBeforeBinaryOperatorError], and returns a + * [NoVariableOrNumberBeforeBinaryOperatorSubject] to test its specific attributes. + */ fun isNoVarOrNumBeforeBinaryOperatorThat(): NoVariableOrNumberBeforeBinaryOperatorSubject { return NoVariableOrNumberBeforeBinaryOperatorSubject.assertThat(verifyAsType()) } + /** + * Verifies that the [MathParsingError] being tested is a + * [NoVariableOrNumberAfterBinaryOperatorError], and returns a + * [NoVariableOrNumberAfterBinaryOperatorSubject] to test its specific attributes. + */ fun isNoVariableOrNumberAfterBinaryOperatorThat(): NoVariableOrNumberAfterBinaryOperatorSubject { return NoVariableOrNumberAfterBinaryOperatorSubject.assertThat(verifyAsType()) } + /** + * Verifies that the [MathParsingError] being tested is an [ExponentIsVariableExpressionError]. + */ fun isExponentIsVariableExpression() { assertThat(actual).isEqualTo(ExponentIsVariableExpressionError) } + /** Verifies that the [MathParsingError] being tested is an [ExponentTooLargeError]. */ fun isExponentTooLarge() { assertThat(actual).isEqualTo(ExponentTooLargeError) } + /** Verifies that the [MathParsingError] being tested is a [NestedExponentsError]. */ fun isNestedExponents() { assertThat(actual).isEqualTo(NestedExponentsError) } + /** Verifies that the [MathParsingError] being tested is a [HangingSquareRootError]. */ fun isHangingSquareRoot() { assertThat(actual).isEqualTo(HangingSquareRootError) } + /** Verifies that the [MathParsingError] being tested is a [TermDividedByZeroError]. */ fun isTermDividedByZero() { assertThat(actual).isEqualTo(TermDividedByZeroError) } + /** Verifies that the [MathParsingError] being tested is a [VariableInNumericExpressionError]. */ fun isVariableInNumericExpression() { assertThat(actual).isEqualTo(VariableInNumericExpressionError) } + /** + * Verifies that the [MathParsingError] being tested is a [DisabledVariablesInUseError], and + * returns an [IterableSubject] to verify the specific disallowed variables in use. + */ fun isDisabledVariablesInUseWithVariablesThat(): IterableSubject { return assertThat(verifyAsType().variables) } + /** Verifies that the [MathParsingError] being tested is an [EquationIsMissingEqualsError]. */ fun isEquationIsMissingEquals() { assertThat(actual).isEqualTo(EquationIsMissingEqualsError) } + /** Verifies that the [MathParsingError] being tested is an [EquationHasTooManyEqualsError]. */ fun isEquationHasTooManyEquals() { assertThat(actual).isEqualTo(EquationHasTooManyEqualsError) } + /** Verifies that the [MathParsingError] being tested is an [EquationMissingLhsOrRhsError]. */ fun isEquationMissingLhsOrRhs() { assertThat(actual).isEqualTo(EquationMissingLhsOrRhsError) } + /** + * Verifies that the [MathParsingError] being tested is a [InvalidFunctionInUseError], and returns + * a [StringSubject] to verify the specific function name in used. + */ fun isInvalidFunctionInUseWithNameThat(): StringSubject { return assertThat(verifyAsType().functionName) } + /** Verifies that the [MathParsingError] being tested is a [FunctionNameIncompleteError]. */ fun isFunctionNameIncomplete() { assertThat(actual).isEqualTo(FunctionNameIncompleteError) } + /** Verifies that the [MathParsingError] being tested is a [GenericError]. */ fun isGenericError() { assertThat(actual).isEqualTo(GenericError) } @@ -143,15 +214,32 @@ class MathParsingErrorSubject( return actual as T } - class SingleRedundantParenthesesSubject( + /** + * Truth subject for verifying properties of [SingleRedundantParenthesesError]s. + * + * Call [assertThat] to create the subject. + */ + class SingleRedundantParenthesesSubject private constructor( metadata: FailureMetadata, private val actual: SingleRedundantParenthesesError ) : Subject(metadata, actual) { + /** + * Returns a [StringSubject] to test the value of + * [SingleRedundantParenthesesError.rawExpression] for the error being tested by this subject. + */ fun hasRawExpressionThat(): StringSubject = assertThat(actual.rawExpression) + /** + * Returns a [MathExpressionSubject] to test the value of + * [SingleRedundantParenthesesError.expression] for the error being tested by this subject. + */ fun hasExpressionThat(): MathExpressionSubject = assertThat(actual.expression) companion object { + /** + * Returns a new [SingleRedundantParenthesesSubject] to verify aspects of the specified + * [SingleRedundantParenthesesError] value. + */ internal fun assertThat( actual: SingleRedundantParenthesesError ): SingleRedundantParenthesesSubject { @@ -160,15 +248,33 @@ class MathParsingErrorSubject( } } - class MultipleRedundantParenthesesSubject( + /** + * Truth subject for verifying properties of [MultipleRedundantParenthesesError]s. + * + * Call [assertThat] to create the subject. + */ + class MultipleRedundantParenthesesSubject private constructor( metadata: FailureMetadata, private val actual: MultipleRedundantParenthesesError ) : Subject(metadata, actual) { + /** + * Returns a [StringSubject] to test the value of + * [MultipleRedundantParenthesesError.rawExpression] for the error being tested by this subject. + */ fun hasRawExpressionThat(): StringSubject = assertThat(actual.rawExpression) + /** + * Returns a [MathExpressionSubject] to test the value of + * [MultipleRedundantParenthesesError.expression] for the error being tested by this + * subject. + */ fun hasExpressionThat(): MathExpressionSubject = assertThat(actual.expression) companion object { + /** + * Returns a new [MultipleRedundantParenthesesSubject] to verify aspects of the specified + * [MultipleRedundantParenthesesError] value. + */ internal fun assertThat( actual: MultipleRedundantParenthesesError ): MultipleRedundantParenthesesSubject { @@ -177,15 +283,34 @@ class MathParsingErrorSubject( } } - class RedundantParenthesesForIndividualTermsSubject( + /** + * Truth subject for verifying properties of [RedundantParenthesesForIndividualTermsError]s. + * + * Call [assertThat] to create the subject. + */ + class RedundantParenthesesForIndividualTermsSubject private constructor( metadata: FailureMetadata, private val actual: RedundantParenthesesForIndividualTermsError ) : Subject(metadata, actual) { + /** + * Returns a [StringSubject] to test the value of + * [RedundantParenthesesForIndividualTermsError.rawExpression] for the error being tested by + * this subject. + */ fun hasRawExpressionThat(): StringSubject = assertThat(actual.rawExpression) + /** + * Returns a [MathExpressionSubject] to test the value of + * [RedundantParenthesesForIndividualTermsError.expression] for the error being tested by this + * subject. + */ fun hasExpressionThat(): MathExpressionSubject = assertThat(actual.expression) companion object { + /** + * Returns a new [RedundantParenthesesForIndividualTermsSubject] to verify aspects of the + * specified [RedundantParenthesesForIndividualTermsError] value. + */ internal fun assertThat( actual: RedundantParenthesesForIndividualTermsError ): RedundantParenthesesForIndividualTermsSubject { @@ -194,29 +319,63 @@ class MathParsingErrorSubject( } } - class NumberAfterVariableSubject( + /** + * Truth subject for verifying properties of [NumberAfterVariableError]s. + * + * Call [assertThat] to create the subject. + */ + class NumberAfterVariableSubject private constructor( metadata: FailureMetadata, private val actual: NumberAfterVariableError ) : Subject(metadata, actual) { + /** + * Returns a [RealSubject] to test the value of [NumberAfterVariableError.number] for the error + * being tested by this subject. + */ fun hasNumberThat(): RealSubject = assertThat(actual.number) + /** + * Returns a [StringSubject] to test the value of [NumberAfterVariableError.variable] for the + * error being tested by this subject. + */ fun hasVariableThat(): StringSubject = assertThat(actual.variable) companion object { + /** + * Returns a new [NumberAfterVariableSubject] to verify aspects of the specified + * [NumberAfterVariableError] value. + */ internal fun assertThat(actual: NumberAfterVariableError): NumberAfterVariableSubject = assertAbout(::NumberAfterVariableSubject).that(actual) } } - class SubsequentBinaryOperatorsSubject( + /** + * Truth subject for verifying properties of [SubsequentBinaryOperatorsError]s. + * + * Call [assertThat] to create the subject. + */ + class SubsequentBinaryOperatorsSubject private constructor( metadata: FailureMetadata, private val actual: SubsequentBinaryOperatorsError ) : Subject(metadata, actual) { + /** + * Returns a [StringSubject] to test the value of [SubsequentBinaryOperatorsError.operator1] for + * the error being tested by this subject. + */ fun hasFirstOperatorThat(): StringSubject = assertThat(actual.operator1) + /** + * Returns a [StringSubject] to test the value of [SubsequentBinaryOperatorsError.operator2] for + * the error being tested by this subject. + */ fun hasSecondOperatorThat(): StringSubject = assertThat(actual.operator2) companion object { + /** + * Returns a new [SubsequentBinaryOperatorsSubject] to verify aspects of the + * specified [SubsequentBinaryOperatorsError] value. + */ internal fun assertThat( actual: SubsequentBinaryOperatorsError ): SubsequentBinaryOperatorsSubject { @@ -225,16 +384,35 @@ class MathParsingErrorSubject( } } - class NoVariableOrNumberBeforeBinaryOperatorSubject( + /** + * Truth subject for verifying properties of [NoVariableOrNumberBeforeBinaryOperatorError]s. + * + * Call [assertThat] to create the subject. + */ + class NoVariableOrNumberBeforeBinaryOperatorSubject private constructor( metadata: FailureMetadata, private val actual: NoVariableOrNumberBeforeBinaryOperatorError ) : Subject(metadata, actual) { + /** + * Returns a [ComparableSubject] to test the value of + * [NoVariableOrNumberBeforeBinaryOperatorError.operator] for the error being tested by this + * subject. + */ fun hasOperatorThat(): ComparableSubject = assertThat(actual.operator) + /** + * Returns a [StringSubject] to test the value of + * [NoVariableOrNumberBeforeBinaryOperatorError.operatorSymbol] for the error being tested by + * this subject. + */ fun hasOperatorSymbolThat(): StringSubject = assertThat(actual.operatorSymbol) companion object { + /** + * Returns a new [NoVariableOrNumberBeforeBinaryOperatorSubject] to verify aspects of the + * specified [NoVariableOrNumberBeforeBinaryOperatorError] value. + */ internal fun assertThat( actual: NoVariableOrNumberBeforeBinaryOperatorError ): NoVariableOrNumberBeforeBinaryOperatorSubject { @@ -243,16 +421,35 @@ class MathParsingErrorSubject( } } - class NoVariableOrNumberAfterBinaryOperatorSubject( + /** + * Truth subject for verifying properties of [NoVariableOrNumberAfterBinaryOperatorError]s. + * + * Call [assertThat] to create the subject. + */ + class NoVariableOrNumberAfterBinaryOperatorSubject private constructor( metadata: FailureMetadata, private val actual: NoVariableOrNumberAfterBinaryOperatorError ) : Subject(metadata, actual) { + /** + * Returns a [ComparableSubject] to test the value of + * [NoVariableOrNumberAfterBinaryOperatorError.operator] for the error being tested by this + * subject. + */ fun hasOperatorThat(): ComparableSubject = assertThat(actual.operator) + /** + * Returns a [StringSubject] to test the value of + * [NoVariableOrNumberAfterBinaryOperatorError.operatorSymbol] for the error being tested by + * this subject. + */ fun hasOperatorSymbolThat(): StringSubject = assertThat(actual.operatorSymbol) companion object { + /** + * Returns a new [NoVariableOrNumberAfterBinaryOperatorSubject] to verify aspects of the + * specified [NoVariableOrNumberAfterBinaryOperatorError] value. + */ internal fun assertThat( actual: NoVariableOrNumberAfterBinaryOperatorError ): NoVariableOrNumberAfterBinaryOperatorSubject { @@ -262,6 +459,10 @@ class MathParsingErrorSubject( } companion object { + /** + * Returns a new [MathParsingErrorSubject] to verify aspects of the specified [MathParsingError] + * value. + */ fun assertThat(actual: MathParsingError): MathParsingErrorSubject = assertAbout(::MathParsingErrorSubject).that(actual) } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt index 25a40605beb..41bc60243d5 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -67,15 +67,23 @@ import org.oppia.android.util.math.MathParsingError.EquationHasTooManyEqualsErro import org.oppia.android.util.math.MathParsingError.EquationIsMissingEqualsError import org.oppia.android.util.math.PeekableIterator.Companion.toPeekableIterator +/** + * Parser for numeric expressions, algebraic expressions, and algebraic equations. + * + * Note that this parser is guaranteed to be LL(1), and to perform a series of robust error checks + * against invalid string expressions. The implementation is specifically designed to ensure an + * LL(1) grammar for both simplicity and long-term maintainability (as it's likely additional + * functionality will need to be added to the language). + * + * To use the parser: + * - Call [parseNumericExpression] for numeric expressions + * - Call [parseAlgebraicExpression] for algebraic expressions + * - Call [parseAlgebraicEquation] for algebraic equations + * + * For the formal grammar specification, see: + * https://docs.google.com/document/d/1JMpbjqRqdEpye67HvDoqBo_rtScY9oEaB7SwKBBspss/edit#bookmark=id.wtmim9gp20a6. + */ class MathExpressionParser private constructor(private val parseContext: ParseContext) { - // TODO: - // - Add helpers to reduce overall parser length. - // - Integrate with new errors & update the routines to not rely on exceptions except in actual exceptional cases. Make sure optional errors can be disabled (for testing purposes). - // - Rename this to be a generic parser, update the public API, add documentation, remove the old classes, and split up the big test routines into actual separate tests. - - // TODO: implement specific errors. - // TODO: verify remaining GenericErrors are correct. - // TODO: document that 'generic' means either 'numeric' or 'algebraic' (ie that the expression is syntactically the same between both grammars). // TODO: document that one design goal is keeping the grammar for this parser as LL(1) & why. @@ -86,6 +94,13 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } } + /** + * Returns a parsed [MathParsingResult] of [MathExpression] from the current [ParseContext]. + * + * Note that 'generic' here and elsewhere means that it can either be a 'numeric' or 'algebraic' + * expression (the specifics are handled lower in the parsing call tree). Generic methods are used + * to share common parsing logic to reduce the overall size of the parser. + */ private fun parseGenericExpressionGrammar(): MathParsingResult { // generic_expression_grammar = generic_expression ; return parseGenericExpression().maybeFail { expression -> checkForLearnerErrors(expression) } @@ -675,16 +690,24 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo private fun InvalidToken.toFailure(): MathParsingResult = toError().toFailure() + /** + * Specification of context while parsing math expressions and equations. + * + * @property rawExpression the whole raw math expression/equation currently being parsed + */ private sealed class ParseContext(val rawExpression: String) { - val tokens: PeekableIterator by lazy { + private val tokens: PeekableIterator by lazy { MathTokenizer.tokenize(rawExpression).toPeekableIterator() } private var previousToken: Token? = null + /** Specifies the [ErrorCheckingMode] for the current parsing context. */ abstract val errorCheckingMode: ErrorCheckingMode + /** Returns whether there are more [Token]s to parse. */ fun hasMoreTokens(): Boolean = tokens.hasNext() + /** Returns the next [Token] available to parse. */ fun peekToken(): Token? = tokens.peek() /** @@ -695,8 +718,13 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo */ fun getPreviousToken(): Token? = previousToken + /** Returns whether the next available token is type [T] (implies there is a token to parse). */ inline fun hasNextTokenOfType(): Boolean = peekToken() is T + /** + * Consumes the next [Token] (which is assumed to be type [T], otherwise the error provided by + * [missingError] is used) and returns the result. + */ inline fun consumeTokenOfType( missingError: () -> MathParsingError = { GenericError } ): MathParsingResult { @@ -707,42 +735,81 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } ?: missingError().toFailure() } + /** Returns the raw string sub-expression corresponding to the specified [Token]. */ fun extractSubexpression(token: Token): String { return rawExpression.substring(token.startIndex, token.endIndex) } + /** Returns the raw string sub-expression corresponding to the specified [MathExpression]. */ fun extractSubexpression(expression: MathExpression): String { return rawExpression.substring(expression.parseStartIndex, expression.parseEndIndex) } + /** The [ParseContext] corresponding to parsing numeric expressions. */ class NumericExpressionContext( rawExpression: String, override val errorCheckingMode: ErrorCheckingMode - ) : ParseContext(rawExpression) { - } + ) : ParseContext(rawExpression) + /** + * The [ParseContext] corresponding to parsing algebraic expressions & equations. + * + * @property isPartOfEquation whether this context is part of parsing an equation + * @property allowedVariables the list of variables allowed to be used within this context + */ class AlgebraicExpressionContext( rawExpression: String, val isPartOfEquation: Boolean, private val allowedVariables: List, override val errorCheckingMode: ErrorCheckingMode ) : ParseContext(rawExpression) { + /** Returns whether the specified variable is allowed to be used per this context. */ fun allowsVariable(variableName: String): Boolean = variableName in allowedVariables } } companion object { + /** The level of error detection strictness that should be enabled during parsing. */ enum class ErrorCheckingMode { + /** + * Indicates that only only irrecoverable errors should be detected. + * + * See the documentation for specific [MathParsingError]s to determine which are + * irrecoverable. + */ REQUIRED_ONLY, + + /** + * Indicates that both irrecoverable and optional errors should be detected (the strictest + * setting). + * + * Note that 'optional' errors are those that correspond to syntaxes that can still be + * correctly represented as a math expression or equation (but may indicate a learner + * misunderstanding). + * + * See the documentation for specific [MathParsingError]s to determine which are optional. + */ ALL_ERRORS } + /** The result of attempting to parse a raw math expression or equation. */ sealed class MathParsingResult { + /** Indicates that the parse was successful with a value of [result]. */ data class Success(val result: T) : MathParsingResult() + /** Indicates that the parse failed with the specified [error]. */ data class Failure(val error: MathParsingError) : MathParsingResult() } + /** + * Parses a [rawExpression] as a numeric expression + * + * Note that the returned expression will have all of its parsing information stripped. + * + * @param errorCheckingMode specifies what level of error detection should be enabled during + * parsing. The default is [ErrorCheckingMode.ALL_ERRORS]. + * @return the result of attempting to parse the specified numeric expression + */ fun parseNumericExpression( rawExpression: String, errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS @@ -752,6 +819,17 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo .map { it.stripParseInfo() } } + /** + * Parses a [rawExpression] as an algebraic expression + * + * Note that the returned expression will have all of its parsing information stripped. + * + * @param allowedVariables the list of case-sensitive variables allowed in the expression (any + * variables encountered that are not within the list will result in an error) + * @param errorCheckingMode specifies what level of error detection should be enabled during + * parsing. The default is [ErrorCheckingMode.ALL_ERRORS]. + * @return the result of attempting to parse the specified algebraic expression + */ fun parseAlgebraicExpression( rawExpression: String, allowedVariables: List, @@ -762,6 +840,17 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo ).parseGenericExpressionGrammar().map { it.stripParseInfo() } } + /** + * Parses a [rawExpression] as an algebraic equation + * + * Note that the returned expression will have all of its parsing information stripped. + * + * @param allowedVariables the list of case-sensitive variables allowed in the expression (any + * variables encountered that are not within the list will result in an error) + * @param errorCheckingMode specifies what level of error detection should be enabled during + * parsing. The default is [ErrorCheckingMode.ALL_ERRORS]. + * @return the result of attempting to parse the specified algebraic equation + */ fun parseAlgebraicEquation( rawExpression: String, allowedVariables: List, @@ -906,11 +995,23 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } } + /** + * Represents the right-hand side of a binary operation. + * + * @property operator the operator corresponding to the operation + * @property rhsResult the pending result for parsing the right-hand side + * @property isImplicit whether this is an implicit operation (such as implicit multiplication) + */ private data class BinaryOperationRhs( val operator: MathBinaryOperation.Operator, val rhsResult: MathParsingResult, val isImplicit: Boolean = false ) { + /** + * Returns the result of combining the left & right-hand sides of the operation into a single + * [MathExpression] representing the entire binary operation (or a failure if either the + * left-hand or right-hand sides failed). + */ fun computeBinaryOperationExpression( lhsResult: MathParsingResult ): MathParsingResult { diff --git a/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt index ec19c7d745b..4fead75b5f2 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt @@ -4,70 +4,239 @@ import org.oppia.android.app.model.MathBinaryOperation import org.oppia.android.app.model.MathExpression import org.oppia.android.app.model.Real +/** + * An error that can be encountered while trying to parse a raw math expression or equation. + * + * All possible errors are subclasses to this sealed class. Further, this class has a dedicated + * Truth test subject that can be used for nicer testing. + */ sealed class MathParsingError { + /** + * Indicates that the user put spaces between two subsequent errors (e.g. '2 2'). + * + * This is an irrecoverable errors since implicit multiplication between numbers is expressly + * prohibited. + */ object SpacesBetweenNumbersError : MathParsingError() + /** + * Indicates that the user didn't finish a parenthetical group, e.g. '(2' or '2)'. + * + * This is an irrecoverable error. + */ object UnbalancedParenthesesError : MathParsingError() + /** + * Indicates that the entire expression has redundant parentheses, e.g. '(2)'. + * + * This is an optional error. + * + * @property rawExpression the raw string expression that has the extra parentheses + * @property expression the parsed sub-expression that has the extra parentheses + */ data class SingleRedundantParenthesesError( val rawExpression: String, val expression: MathExpression ) : MathParsingError() + /** + * Indicates that the entire expression or a sub-term has multiple redundant parentheses, e.g. + * '((2))' or '((2)) + 1'. + * + * This is an optional error. + * + * @property rawExpression the raw string expression that has the extra parentheses + * @property expression the parsed sub-expression that has the extra parentheses + */ data class MultipleRedundantParenthesesError( val rawExpression: String, val expression: MathExpression ) : MathParsingError() + /** + * Indicates that a sub-term of the expression has unnecessary parentheses, e.g. '(2) + 1'. + * + * This is an optional error. + * + * @property rawExpression the raw string expression that has the extra parentheses + * @property expression the parsed sub-expression that has the extra parentheses + */ data class RedundantParenthesesForIndividualTermsError( val rawExpression: String, val expression: MathExpression ) : MathParsingError() + /** + * Indicates that an invalid symbol was encountered while parsing, e.g. '@'. + * + * This is an irrecoverable error. + * + * @property invalidSymbol the raw invalid symbol that was encountered during parsing + */ data class UnnecessarySymbolsError(val invalidSymbol: String) : MathParsingError() + /** + * Indicates that a number was encountered to the right of a variable, e.g. 'x2'. + * + * This is an irrecoverable error since the grammar specifically prohibits implicit multiplication + * of numbers on the right side. + * + * @property number the number that was parsed on the right side of the variable + * @property variable the variable to whose right is a number + */ data class NumberAfterVariableError(val number: Real, val variable: String) : MathParsingError() + /** + * Indicates that two binary operators were encountered with nothing between, e.g. '1 +* 2'. + * + * This is an irrecoverable error. + * + * @property operator1 the first (left) operator encountered + * @property operator2 the second (right) operator encountered + */ data class SubsequentBinaryOperatorsError( val operator1: String, val operator2: String ) : MathParsingError() + /** + * Indicates that two unary operators were encountered with nothing between, e.g. '--2'. + * + * This is an irrecoverable error. + */ object SubsequentUnaryOperatorsError : MathParsingError() + /** + * Indicates that a binary operator was encountered without a left-hand operand, e.g. '*2'. + * + * This is a generally an irrecoverable error. + * + * Note that operators that are both unary and binary (e.g. '-') cannot trigger this error since + * such cases will be correctly interpreted as a unary operation. + * + * Further, this error has one optional case for unary plus operators (i.e. with strict error + * checking '+2' will result in this error, but is otherwise valid in non-strict mode). + * + * @property operator the operator to whose left is no operand + * @property operatorSymbol the raw symbol used to represent [operator] (which can't be assumed as + * any particular value since multiple symbols can correspond to a single operator) + */ data class NoVariableOrNumberBeforeBinaryOperatorError( val operator: MathBinaryOperation.Operator, val operatorSymbol: String ) : MathParsingError() + /** + * Indicates that a binary operator was encountered without a right-hand operand, e.g. '2+'. + * + * This is an irrecoverable error. + * + * @property operator the operator to whose right is no operand + * @property operatorSymbol the raw symbol used to represent [operator] (which can't be assumed as + * any particular value since multiple symbols can correspond to a single operator) + */ data class NoVariableOrNumberAfterBinaryOperatorError( val operator: MathBinaryOperation.Operator, val operatorSymbol: String ) : MathParsingError() + /** + * Indicates that an exponent has a variable in its power, e.g. '2^x'. + * + * This is an optional error to help enforce polynomial syntax. + */ object ExponentIsVariableExpressionError : MathParsingError() + /** + * Indicates that an exponent's power is too large, e.g. '3^1000'. + * + * This is an optional error to help avoid calculation overflow for certain answer classifiers. + */ object ExponentTooLargeError : MathParsingError() + /** + * Indicates that an exponent's power is another exponent, e.g. '2^3^4'. + * + * This is an optional error to help avoid calculation overflow or potential learner mistakes. + */ object NestedExponentsError : MathParsingError() + /** + * Indicates that no value was found after a square root, e.g. '2√'. + * + * This is an irrecoverable error. + */ object HangingSquareRootError : MathParsingError() + /** + * Indicates that a value is being divided by zero, e.g. '2/0'. + * + * This is an optional error that helps avoid automatic failure in classifiers. Note that the + * absence of this error does not guarantee the expression has no divide-by-zeros (since it only + * performs a non-evaluative cursory check). + */ object TermDividedByZeroError : MathParsingError() + /** + * Indicates that a variable was encountered in a numeric-only expression, e.g. '1+3-x'. + * + * This is an irrecoverable error. + */ object VariableInNumericExpressionError : MathParsingError() + /** + * Indicates that one or more non-whitelisted variables were encountered in an algebraic + * expression. + * + * This is an optional error. + * + * @param variables the list of variables from the expression that aren't allowed + */ data class DisabledVariablesInUseError(val variables: List) : MathParsingError() + /** + * Indicates that an algebraic equation is missing an equals sign, e.g. '4 x'. + * + * This is an irrecoverable error. + */ object EquationIsMissingEqualsError: MathParsingError() + /** + * Indicates that an algebraic equation has too many equals signs, e.g. '4 == x'. + * + * This is an irrecoverable error. + */ object EquationHasTooManyEqualsError: MathParsingError() + /** + * Indicates that an algebraic equation is missing either its left or right side, e.g. '4=' and + * '=x'. + * + * This is an irrecoverable error. + */ object EquationMissingLhsOrRhsError : MathParsingError() + /** + * Indicates that a recognized disabled function was used, e.g. 'abs(x)'. + * + * This is an irrecoverable error since the proto structure for math expressions is strictly + * limited to supported functions. + * + * @param functionName the name of the used prohibited function + */ data class InvalidFunctionInUseError(val functionName: String) : MathParsingError() + /** + * Indicates that a function name was started, but not completed, e.g.: 'sqr(2)'. + * + * This is an irrecoverable error. + */ object FunctionNameIncompleteError : MathParsingError() + /** + * Indicates a generic error that wasn't specifically recognized as any of the others. + * + * This is an irrecoverable error, though it may be triggered by trying to find optional errors. + */ object GenericError : MathParsingError() } From aeec35cca2cab12a78d0bf15f3dd79c4372d8624 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 24 Jan 2022 17:55:57 -0800 Subject: [PATCH 058/134] Lint fixes. --- .../oppia/android/util/math/MathExpressionParser.kt | 6 +++--- .../org/oppia/android/util/math/MathParsingError.kt | 4 ++-- .../util/math/AlgebraicEquationParserTest.kt | 5 +++-- .../util/math/AlgebraicExpressionParserTest.kt | 10 +++++++--- .../android/util/math/MathExpressionParserTest.kt | 13 ++++++++----- .../util/math/NumericExpressionParserTest.kt | 3 ++- 6 files changed, 25 insertions(+), 16 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt index 41bc60243d5..2cd252618ea 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -23,6 +23,8 @@ import org.oppia.android.app.model.Real import org.oppia.android.util.math.MathExpressionParser.ParseContext.AlgebraicExpressionContext import org.oppia.android.util.math.MathExpressionParser.ParseContext.NumericExpressionContext import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError +import org.oppia.android.util.math.MathParsingError.EquationHasTooManyEqualsError +import org.oppia.android.util.math.MathParsingError.EquationIsMissingEqualsError import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError import org.oppia.android.util.math.MathParsingError.ExponentIsVariableExpressionError import org.oppia.android.util.math.MathParsingError.ExponentTooLargeError @@ -61,11 +63,9 @@ import org.oppia.android.util.math.MathTokenizer.Companion.Token.PositiveRealNum import org.oppia.android.util.math.MathTokenizer.Companion.Token.RightParenthesisSymbol import org.oppia.android.util.math.MathTokenizer.Companion.Token.SquareRootSymbol import org.oppia.android.util.math.MathTokenizer.Companion.Token.VariableName +import org.oppia.android.util.math.PeekableIterator.Companion.toPeekableIterator import kotlin.math.absoluteValue import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator -import org.oppia.android.util.math.MathParsingError.EquationHasTooManyEqualsError -import org.oppia.android.util.math.MathParsingError.EquationIsMissingEqualsError -import org.oppia.android.util.math.PeekableIterator.Companion.toPeekableIterator /** * Parser for numeric expressions, algebraic expressions, and algebraic equations. diff --git a/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt index 4fead75b5f2..8bb587c0660 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt @@ -199,14 +199,14 @@ sealed class MathParsingError { * * This is an irrecoverable error. */ - object EquationIsMissingEqualsError: MathParsingError() + object EquationIsMissingEqualsError : MathParsingError() /** * Indicates that an algebraic equation has too many equals signs, e.g. '4 == x'. * * This is an irrecoverable error. */ - object EquationHasTooManyEqualsError: MathParsingError() + object EquationHasTooManyEqualsError : MathParsingError() /** * Indicates that an algebraic equation is missing either its left or right side, e.g. '4=' and diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt index 01f0aeddc27..fa0ccedbb17 100644 --- a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt @@ -1,7 +1,7 @@ package org.oppia.android.util.math -import com.google.common.truth.Truth.assertThat 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.app.model.MathEquation @@ -258,7 +258,8 @@ class AlgebraicEquationParserTest { private companion object { private fun parseAlgebraicEquation( - expression: String, allowedVariables: List = listOf("x", "y", "z") + expression: String, + allowedVariables: List = listOf("x", "y", "z") ): MathEquation { val result = MathExpressionParser.parseAlgebraicEquation( expression, allowedVariables, ErrorCheckingMode.ALL_ERRORS diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt index 7950c52b336..be190a7a520 100644 --- a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt @@ -943,7 +943,8 @@ class AlgebraicExpressionParserTest { private companion object { private fun parseAlgebraicExpressionWithoutOptionalErrors( - expression: String, allowedVariables: List = listOf("x", "y", "z") + expression: String, + allowedVariables: List = listOf("x", "y", "z") ): MathExpression { return parseAlgebraicExpressionInternal( expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables @@ -951,7 +952,8 @@ class AlgebraicExpressionParserTest { } private fun parseAlgebraicExpressionWithAllErrors( - expression: String, allowedVariables: List = listOf("x", "y", "z") + expression: String, + allowedVariables: List = listOf("x", "y", "z") ): MathExpression { return parseAlgebraicExpressionInternal( expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables @@ -959,7 +961,9 @@ class AlgebraicExpressionParserTest { } private fun parseAlgebraicExpressionInternal( - expression: String, errorCheckingMode: ErrorCheckingMode, allowedVariables: List + expression: String, + errorCheckingMode: ErrorCheckingMode, + allowedVariables: List ): MathExpression { val result = MathExpressionParser.parseAlgebraicExpression( expression, allowedVariables, errorCheckingMode diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt index 5a37e71a372..198d24551b4 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -993,7 +993,6 @@ class MathExpressionParserTest { val expression = "$func(0.5+1)" val error = expectFailureWhenParsingAlgebraicExpression(expression) - // Starting a detected function but not completing it should result in an incomplete name error. assertThat(error).isFunctionNameIncomplete() } @@ -1092,7 +1091,8 @@ class MathExpressionParserTest { ) private fun expectSuccessWhenParsingNumericExpression( - expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + expression: String, + errorCheckingMode: ErrorCheckingMode = ALL_ERRORS ) { expectSuccessfulParsingResult(parseNumericExpression(expression, errorCheckingMode)) } @@ -1112,7 +1112,8 @@ class MathExpressionParserTest { } private fun expectFailureWhenParsingAlgebraicExpression( - expression: String, allowedVariables: List = listOf("x", "y", "z") + expression: String, + allowedVariables: List = listOf("x", "y", "z") ): MathParsingError { return expectFailingParsingResult(parseAlgebraicExpression(expression, allowedVariables)) } @@ -1126,13 +1127,15 @@ class MathExpressionParserTest { } private fun parseNumericExpression( - expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + expression: String, + errorCheckingMode: ErrorCheckingMode = ALL_ERRORS ): MathParsingResult { return MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) } private fun parseAlgebraicExpression( - expression: String, allowedVariables: List, + expression: String, + allowedVariables: List, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS ): MathParsingResult { return MathExpressionParser.parseAlgebraicExpression( diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index 42971dc5d57..b34df9875f6 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -1576,7 +1576,8 @@ class NumericExpressionParserTest { parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) private fun parseNumericExpressionInternal( - expression: String, errorCheckingMode: ErrorCheckingMode + expression: String, + errorCheckingMode: ErrorCheckingMode ): MathExpression { val result = MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) assertThat(result).isInstanceOf(MathParsingResult.Success::class.java) From 57e6f5b889e621a1ba83630df950a2e289e86805 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 24 Jan 2022 18:26:26 -0800 Subject: [PATCH 059/134] Remove temporary TODOs. --- .../java/org/oppia/android/util/math/MathExpressionParser.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt index 2cd252618ea..20b44cee192 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -84,9 +84,6 @@ import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator * https://docs.google.com/document/d/1JMpbjqRqdEpye67HvDoqBo_rtScY9oEaB7SwKBBspss/edit#bookmark=id.wtmim9gp20a6. */ class MathExpressionParser private constructor(private val parseContext: ParseContext) { - // TODO: document that 'generic' means either 'numeric' or 'algebraic' (ie that the expression is syntactically the same between both grammars). - // TODO: document that one design goal is keeping the grammar for this parser as LL(1) & why. - private fun parseGenericEquationGrammar(): MathParsingResult { // generic_equation_grammar = generic_equation ; return parseGenericEquation().maybeFail { equation -> From 07be5960af2d894773bbb8c2263ffd8bfda50100 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 26 Jan 2022 15:43:52 -0800 Subject: [PATCH 060/134] Add tests. --- .../oppia/android/testing/math/RealSubject.kt | 16 +- .../org/oppia/android/util/math/BUILD.bazel | 18 +- .../android/util/math/FractionExtensions.kt | 10 +- .../oppia/android/util/math/FractionParser.kt | 72 + .../oppia/android/util/math/RealExtensions.kt | 79 +- .../org/oppia/android/util/math/BUILD.bazel | 58 +- .../math/ExpressionToLatexConverterTest.kt | 215 +++ .../util/math/ExpressionToLatexTest.kt | 123 -- .../util/math/FractionExtensionsTest.kt | 887 +++++++++++ .../util/math/MathExpressionExtensionsTest.kt | 97 ++ .../math/NumericExpressionEvaluatorTest.kt | 239 +++ .../util/math/NumericExpressionParserTest.kt | 90 +- .../android/util/math/RealExtensionsTest.kt | 1385 ++++++++++++++++- 13 files changed, 3112 insertions(+), 177 deletions(-) create mode 100644 utility/src/main/java/org/oppia/android/util/math/FractionParser.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt delete mode 100644 utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexTest.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/NumericExpressionEvaluatorTest.kt diff --git a/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt index bb8a180b306..908bc7be6e2 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt @@ -22,15 +22,17 @@ import org.oppia.android.testing.math.FractionSubject.Companion.assertThat */ class RealSubject private constructor( metadata: FailureMetadata, - private val actual: Real + private val actual: Real? ) : LiteProtoSubject(metadata, actual) { + private val nonNullActual by lazy { checkNotNull(actual) { "Expected real to be non-null" } } + /** * Returns a [FractionSubject] to test [Real.getRational]. This will fail if the [Real] pertaining * to this subject is not of type rational. */ fun isRationalThat(): FractionSubject { verifyTypeToBe(Real.RealTypeCase.RATIONAL) - return assertThat(actual.rational) + return assertThat(nonNullActual.rational) } /** @@ -39,7 +41,7 @@ class RealSubject private constructor( */ fun isIrrationalThat(): DoubleSubject { verifyTypeToBe(Real.RealTypeCase.IRRATIONAL) - return assertThat(actual.irrational) + return assertThat(nonNullActual.irrational) } /** @@ -48,17 +50,17 @@ class RealSubject private constructor( */ fun isIntegerThat(): IntegerSubject { verifyTypeToBe(Real.RealTypeCase.INTEGER) - return assertThat(actual.integer) + return assertThat(nonNullActual.integer) } private fun verifyTypeToBe(expected: Real.RealTypeCase) { - assertWithMessage("Expected real type to be $expected, not: ${actual.realTypeCase}") - .that(actual.realTypeCase) + assertWithMessage("Expected real type to be $expected, not: ${nonNullActual.realTypeCase}") + .that(nonNullActual.realTypeCase) .isEqualTo(expected) } companion object { /** Returns a new [RealSubject] to verify aspects of the specified [Real] value. */ - fun assertThat(actual: Real): RealSubject = assertAbout(::RealSubject).that(actual) + fun assertThat(actual: Real?): RealSubject = assertAbout(::RealSubject).that(actual) } } 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 index 1f2e9313754..00b17035b03 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -33,12 +33,12 @@ kt_android_library( ) kt_android_library( - name = "parser", + name = "math_expression_parser", srcs = [ "MathExpressionParser.kt", ], visibility = [ - "//:oppia_testing_visibility", + "//:oppia_api_visibility", ], deps = [ ":parsing_error", @@ -49,6 +49,20 @@ kt_android_library( ], ) +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", + ], +) + kt_android_library( name = "tokenizer", srcs = [ diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index 7457b329777..77e1e7ef420 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -106,7 +106,7 @@ fun Fraction.toImproperForm(): Fraction { } /** Returns the inverse improper fraction representation of this fraction. */ -fun Fraction.toInvertedImproperForm(): Fraction { +private fun Fraction.toInvertedImproperForm(): Fraction { return toImproperForm().let { improper -> improper.toBuilder().apply { numerator = improper.denominator @@ -188,7 +188,8 @@ operator fun Fraction.div(rhs: Fraction): Fraction { return this * rhs.toInvertedImproperForm() } -fun Fraction.pow(exp: Int): Fraction { +// TODO: document 0^0 case. +infix fun Fraction.pow(exp: Int): Fraction { return when { exp == 0 -> { Fraction.newBuilder().apply { @@ -198,11 +199,11 @@ fun Fraction.pow(exp: Int): Fraction { } exp == 1 -> this // x^-2 == 1/(x^2). - exp < 1 -> pow(-exp).toInvertedImproperForm().toProperForm() + exp < 1 -> (this pow -exp).toInvertedImproperForm().toProperForm() else -> { // i > 1 var newValue = this for (i in 1 until exp) newValue *= this - return newValue + return newValue.toProperForm() } } } @@ -218,6 +219,7 @@ fun Int.toWholeNumberFraction(): Fraction { }.build() } + /** Returns the greatest common divisor between two integers. */ private fun gcd(x: Int, y: Int): Int { return if (y == 0) x else gcd(y, x % y) diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionParser.kt b/utility/src/main/java/org/oppia/android/util/math/FractionParser.kt new file mode 100644 index 00000000000..e5f30a36fbc --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/FractionParser.kt @@ -0,0 +1,72 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.Fraction +import org.oppia.android.domain.util.normalizeWhitespace + +/** String parser for [Fraction]s. */ +class FractionParser private constructor() { + companion object { + private val wholeNumberOnlyRegex = """^-? ?(\d+)$""".toRegex() + private val fractionOnlyRegex = """^-? ?(\d+) ?/ ?(\d+)$""".toRegex() + private val mixedNumberRegex = """^-? ?(\d+) (\d+) ?/ ?(\d+)$""".toRegex() + + /** + * Returns a [Fraction] parse from the specified raw text string. + * + * Unlike [tryParseFraction] this function will throw if the provided text is invalid. + */ + fun parseFraction(text: String): Fraction { + return tryParseFraction(text) + ?: throw IllegalArgumentException("Incorrectly formatted fraction: $text") + } + + /** + * Returns a [Fraction] parse from the specified raw text string, or null if the provided text + * doesn't correctly represent a fraction. + */ + fun tryParseFraction(text: String): Fraction? { + // Normalize whitespace to ensure that answer follows a simpler subset of possible patterns. + val inputText: String = text.normalizeWhitespace() + return parseMixedNumber(inputText) + ?: parseRegularFraction(inputText) + ?: parseWholeNumber(inputText) + } + + private fun parseMixedNumber(inputText: String): Fraction? { + val mixedNumberMatch = mixedNumberRegex.matchEntire(inputText) ?: return null + val (_, wholeNumberText, numeratorText, denominatorText) = + mixedNumberMatch.groupValues + return Fraction.newBuilder() + .setIsNegative(isInputNegative(inputText)) + .setWholeNumber(wholeNumberText.toInt()) + .setNumerator(numeratorText.toInt()) + .setDenominator(denominatorText.toInt()) + .build() + } + + private fun parseRegularFraction(inputText: String): Fraction? { + val fractionOnlyMatch = fractionOnlyRegex.matchEntire(inputText) ?: return null + val (_, numeratorText, denominatorText) = fractionOnlyMatch.groupValues + // Fraction-only numbers imply no whole number. + return Fraction.newBuilder() + .setIsNegative(isInputNegative(inputText)) + .setNumerator(numeratorText.toInt()) + .setDenominator(denominatorText.toInt()) + .build() + } + + private fun parseWholeNumber(inputText: String): Fraction? { + val wholeNumberMatch = wholeNumberOnlyRegex.matchEntire(inputText) ?: return null + val (_, wholeNumberText) = wholeNumberMatch.groupValues + // Whole number fractions imply '0/1' fractional parts. + return Fraction.newBuilder() + .setIsNegative(isInputNegative(inputText)) + .setWholeNumber(wholeNumberText.toInt()) + .setNumerator(0) + .setDenominator(1) + .build() + } + + private fun isInputNegative(inputText: String): Boolean = inputText.startsWith("-") + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 11251f1060c..8fbff687a66 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -105,7 +105,15 @@ operator fun Real.div(rhs: Real): Real { ) } -fun Real.pow(rhs: Real): Real { +// TODO: document that roots represents the real value representation vs. principal root. Also, +// document 0^0 case per https://stackoverflow.com/a/19955996. +// Rules: +// - Anything involving a double always becomes a double. +// - Int^Int stays int unless it's negative (then it becomes a fraction) +// - Int^Fraction is treated as a fraction power & root (it becomes fraction or double) +// - Fraction^Int always yields a fraction +// - Fraction^Fraction yields a fraction or double (depending on the denominator root) +infix fun Real.pow(rhs: Real): Real { // Powers can really only be effectively done via floats or whole-number only fractions. return when (realTypeCase) { RATIONAL -> { @@ -113,11 +121,11 @@ fun Real.pow(rhs: Real): Real { when (rhs.realTypeCase) { // Anything raised by a fraction is pow'd by the numerator and rooted by the denominator. RATIONAL -> rhs.rational.toImproperForm().let { power -> - rational.pow(power.numerator).root(power.denominator, power.isNegative) + (rational pow power.numerator).root(power.denominator, power.isNegative) } IRRATIONAL -> recompute { it.setIrrational(rational.pow(rhs.irrational)) } - INTEGER -> recompute { it.setRational(rational.pow(rhs.integer)) } - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + INTEGER -> recompute { it.setRational(rational pow rhs.integer) } + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") } } IRRATIONAL -> { @@ -126,7 +134,7 @@ fun Real.pow(rhs: Real): Real { RATIONAL -> recompute { it.setIrrational(irrational.pow(rhs.rational)) } IRRATIONAL -> recompute { it.setIrrational(irrational.pow(rhs.irrational)) } INTEGER -> recompute { it.setIrrational(irrational.pow(rhs.integer)) } - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") } } INTEGER -> { @@ -135,16 +143,16 @@ fun Real.pow(rhs: Real): Real { // An integer raised to a fraction can use the same approach as above (fraction raised to // fraction) by treating the integer as a whole number fraction. RATIONAL -> rhs.rational.toImproperForm().let { power -> - integer.toWholeNumberFraction() - .pow(power.numerator) - .root(power.denominator, power.isNegative) + (integer.toWholeNumberFraction() pow power.numerator).root( + power.denominator, power.isNegative + ) } IRRATIONAL -> recompute { it.setIrrational(integer.toDouble().pow(rhs.irrational)) } INTEGER -> integer.pow(rhs.integer) - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") } } - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $this.") } } @@ -153,7 +161,7 @@ fun sqrt(real: Real): Real { RATIONAL -> sqrt(real.rational) IRRATIONAL -> real.recompute { it.setIrrational(kotlin.math.sqrt(real.irrational)) } INTEGER -> sqrt(real.integer) - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $real.") + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $real.") } } @@ -199,7 +207,7 @@ private fun Int.divide(rhs: Int): Real = Real.newBuilder().apply { isNegative = (lhs < 0) xor (rhs < 0) numerator = kotlin.math.abs(lhs) denominator = kotlin.math.abs(rhs) - }.build() + }.build().toProperForm() } }.build() @@ -208,9 +216,9 @@ private fun Fraction.pow(rhs: Double): Double = toDouble().pow(rhs) private fun Int.pow(exp: Int): Real { return when { - exp == 0 -> Real.newBuilder().apply { integer = 0 }.build() + exp == 0 -> Real.newBuilder().apply { integer = 1 }.build() exp == 1 -> Real.newBuilder().apply { integer = this@pow }.build() - exp < 0 -> Real.newBuilder().apply { rational = toWholeNumberFraction().pow(exp) }.build() + exp < 0 -> Real.newBuilder().apply { rational = toWholeNumberFraction() pow exp }.build() else -> { // exp > 1 var computed = this @@ -223,7 +231,7 @@ private fun Int.pow(exp: Int): Real { private fun sqrt(fraction: Fraction): Real = fraction.root(base = 2, invert = false) private fun Fraction.root(base: Int, invert: Boolean): Real { - check(base > 1) { "Expected base of 2 or higher, not: $base" } + check(base > 0) { "Expected base of 1 or higher, not: $base" } val adjustedFraction = toImproperForm() val adjustedNum = @@ -253,16 +261,39 @@ private fun sqrt(int: Int): Real = root(int, base = 2) private fun root(int: Int, base: Int): Real { // First, check if the integer is a root. Base reference for possible methods: // https://www.researchgate.net/post/How-to-decide-if-a-given-number-will-have-integer-square-root-or-not. - check(base > 1) { "Expected base of 2 or higher, not: $base" } - check((int < 0 && base.isOdd()) || int >= 0) { "Radicand results in imaginary number: $int" } - - if (int == 1) { - // 1^x is always 1. + if (int == 0 && base == 0) { + // This is considered a conventional identity per https://stackoverflow.com/a/19955996 that + // doesn't match mathematics definitions (but it does bring parity with the system's pow() + // function). return Real.newBuilder().apply { integer = 1 }.build() } + check(base > 0) { "Expected base of 1 or higher, not: $base" } + check((int < 0 && base.isOdd()) || int >= 0) { "Radicand results in imaginary number: $int" } + + when { + int == 0 -> { + // 0^x is always zero. + return Real.newBuilder().apply { + integer = 0 + }.build() + } + int == 1 || int == 0 || base == 0 -> { + // 1^x and x^0 are always 1. + return Real.newBuilder().apply { + integer = 1 + }.build() + } + base == 1 -> { + // x^1 is always x. + return Real.newBuilder().apply { + integer = int + }.build() + } + } + val radicand = int.absoluteValue var potentialRoot = base while (potentialRoot.pow(base).integer < radicand) { @@ -319,7 +350,7 @@ private fun combine( } INTEGER -> lhs.recompute { it.setRational(leftRationalRightIntegerOp(lhs.rational, rhs.integer)) } - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") } } IRRATIONAL -> { @@ -337,7 +368,7 @@ private fun combine( lhs.recompute { it.setIrrational(leftIrrationalRightIntegerOp(lhs.irrational, rhs.integer)) } - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") } } INTEGER -> { @@ -350,9 +381,9 @@ private fun combine( it.setIrrational(leftIntegerRightIrrationalOp(lhs.integer, rhs.irrational)) } INTEGER -> leftIntegerRightIntegerOp(lhs.integer, rhs.integer) - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") } } - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $lhs.") + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $lhs.") } } 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 index 5e11de885bb..a557f383254 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -19,7 +19,7 @@ oppia_android_test( "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:parser", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) @@ -38,15 +38,15 @@ oppia_android_test( "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:parser", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) oppia_android_test( - name = "ExpressionToLatexTest", - srcs = ["ExpressionToLatexTest.kt"], + name = "ExpressionToLatexConverterTest", + srcs = ["ExpressionToLatexConverterTest.kt"], custom_package = "org.oppia.android.util.math", - test_class = "org.oppia.android.util.math.ExpressionToLatexTest", + test_class = "org.oppia.android.util.math.ExpressionToLatexConverterTest", test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", @@ -58,7 +58,7 @@ oppia_android_test( "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:parser", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) @@ -98,6 +98,25 @@ oppia_android_test( ], ) +oppia_android_test( + name = "MathExpressionExtensionsTest", + srcs = ["MathExpressionExtensionsTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.MathExpressionExtensionsTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:real_subject", + "//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", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", + ], +) + oppia_android_test( name = "MathExpressionParserTest", srcs = ["MathExpressionParserTest.kt"], @@ -114,7 +133,7 @@ oppia_android_test( "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:parser", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) @@ -138,6 +157,25 @@ oppia_android_test( ], ) +oppia_android_test( + name = "NumericExpressionEvaluatorTest", + srcs = ["NumericExpressionEvaluatorTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.NumericExpressionEvaluatorTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing:assertion_helpers", + "//testing/src/main/java/org/oppia/android/testing/math:real_subject", + "//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:math_expression_parser", + ], +) + oppia_android_test( name = "NumericExpressionParserTest", srcs = ["NumericExpressionParserTest.kt"], @@ -153,7 +191,7 @@ oppia_android_test( "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:parser", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) @@ -219,12 +257,14 @@ oppia_android_test( deps = [ "//model/src/main/proto:math_java_proto_lite", "//testing", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", "//testing/src/main/java/org/oppia/android/testing/math:real_subject", - "//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", + "//utility/src/main/java/org/oppia/android/util/math:fraction_parser", ], ) diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt new file mode 100644 index 00000000000..87be7c78451 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt @@ -0,0 +1,215 @@ +package org.oppia.android.util.math + +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.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat +import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.robolectric.annotation.LooperMode + +/** Tests for [ExpressionToLatexConverter]. */ +// FunctionName: test names are conventionally named with underscores. +// SameParameterValue: tests should have specific context included/excluded for readability. +@Suppress("FunctionName", "SameParameterValue") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class ExpressionToLatexConverterTest { + @Test + fun testConvert_numericExp_number_returnsConstantLatex() { + val exp = parseNumericExpressionWithAllErrors("1") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("1") + } + + @Test + fun testConvert_numericExp_unaryPlus_withoutOptionalErrors_returnLatexWithUnaryPlus() { + val exp = parseNumericExpressionWithoutOptionalErrors("+1") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("+1") + } + + @Test + fun testConvert_numericExp_unaryMinus_returnLatexWithUnaryMinus() { + val exp = parseNumericExpressionWithAllErrors("-1") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("-1") + } + + @Test + fun testConvert_numericExp_addition_returnsLatexWithAddition() { + val exp = parseNumericExpressionWithAllErrors("1+2") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("1 + 2") + } + + @Test + fun testConvert_numericExp_subtraction_returnsLatexWithSubtract() { + val exp = parseNumericExpressionWithAllErrors("1-2") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("1 - 2") + } + + @Test + fun testConvert_numericExp_multiplication_returnsLatexWithMultiplication() { + val exp = parseNumericExpressionWithAllErrors("2*3") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("2 \\times 3") + } + + @Test + fun testConvert_numericExp_division_returnsLatexWithDivision() { + val exp = parseNumericExpressionWithAllErrors("2/3") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("2 \\div 3") + } + + @Test + fun testConvert_numericExp_division_divAsFraction_returnsLatexWithFraction() { + val exp = parseNumericExpressionWithAllErrors("2/3") + + assertThat(exp).convertsWithFractionsToLatexStringThat().isEqualTo("\\frac{2}{3}") + } + + @Test + fun testConvert_numericExp_multipleDivisions_divAsFraction_returnsLatexWithFractions() { + val exp = parseNumericExpressionWithAllErrors("2/3/4") + + assertThat(exp).convertsWithFractionsToLatexStringThat().isEqualTo("\\frac{\\frac{2}{3}}{4}") + } + + @Test + fun testConvert_numericExp_exponent_returnsLatexWithExponent() { + val exp = parseNumericExpressionWithAllErrors("2^3") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("2 ^ {3}") + } + + @Test + fun testConvert_numericExp_inlineSquareRoot_returnsLatexWithSquareRoot() { + val exp = parseNumericExpressionWithAllErrors("√2") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("\\sqrt{2}") + } + + @Test + fun testConvert_numericExp_inlineSquareRoot_operationArg_returnsLatexWithSquareRoot() { + val exp = parseNumericExpressionWithAllErrors("√(1+2)") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("\\sqrt{(1 + 2)}") + } + + @Test + fun testConvert_numericExp_squareRoot_returnsLatexWithSquareRoot() { + val exp = parseNumericExpressionWithAllErrors("sqrt(2)") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("\\sqrt{2}") + } + + @Test + fun testConvert_numericExp_squareRoot_operationArg_returnsLatexWithSquareRoot() { + val exp = parseNumericExpressionWithAllErrors("sqrt(1+2)") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("\\sqrt{1 + 2}") + } + + @Test + fun testConvert_numericExp_parentheses_returnsLatexWithGroup() { + val exp = parseNumericExpressionWithAllErrors("2/(3+4)") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("2 \\div (3 + 4)") + } + + @Test + fun testConvert_numericExp_exponentToGroup_returnsCorrectlyWrappedLatex() { + val exp = parseNumericExpressionWithAllErrors("2^(7-3)") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("2 ^ {(7 - 3)}") + } + + @Test + fun testConvert_algebraicExp_variable_returnsVariableLatex() { + val exp = parseAlgebraicExpressionWithAllErrors("x") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("x") + } + + @Test + fun testConvert_algebraicExp_twoX_returnsLatexWithImplicitMultiplication() { + val exp = parseAlgebraicExpressionWithAllErrors("2x") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("2x") + } + + @Test + fun testConvert_algebraicEq_xEqualsOne_returnsLatexWithEquals() { + val exp = parseAlgebraicEquationWithAllErrors("x=1") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("x = 1") + } + + @Test + fun testConvert_algebraicEq_complexExpression_returnsCorrectLatex() { + val exp = parseAlgebraicEquationWithAllErrors("(x+1)(x-2)=(x^3+2x^2-5x-6)/(x+3)") + + assertThat(exp) + .convertsToLatexStringThat() + .isEqualTo("(x + 1)(x - 2) = (x ^ {3} + 2x ^ {2} - 5x - 6) \\div (x + 3)") + } + + @Test + fun testConvert_algebraicEq_complexExpression_divAsFraction_returnsCorrectLatex() { + val exp = parseAlgebraicEquationWithAllErrors("(x+1)(x-2)=(x^3+2x^2-5x-6)/(x+3)") + + assertThat(exp) + .convertsWithFractionsToLatexStringThat() + .isEqualTo("(x + 1)(x - 2) = \\frac{(x ^ {3} + 2x ^ {2} - 5x - 6)}{(x + 3)}") + } + + private companion object { + private fun parseNumericExpressionWithoutOptionalErrors(expression: String): MathExpression { + return parseNumericExpressionInternal(expression, ErrorCheckingMode.REQUIRED_ONLY) + } + + private fun parseNumericExpressionWithAllErrors(expression: String): MathExpression { + return parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) + } + + private fun parseAlgebraicExpressionWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables, + ErrorCheckingMode.ALL_ERRORS + ).getExpectedSuccess() + } + + private fun parseNumericExpressionInternal( + expression: String, errorCheckingMode: ErrorCheckingMode + ): MathExpression { + return MathExpressionParser.parseNumericExpression( + expression, errorCheckingMode + ).getExpectedSuccess() + } + + private fun parseAlgebraicEquationWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathEquation { + return MathExpressionParser.parseAlgebraicEquation( + expression, allowedVariables, + ErrorCheckingMode.ALL_ERRORS + ).getExpectedSuccess() + } + + private inline fun MathParsingResult.getExpectedSuccess(): T { + assertThat(this).isInstanceOf(MathParsingResult.Success::class.java) + return (this as MathParsingResult.Success).result + } + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexTest.kt deleted file mode 100644 index e56db9a7500..00000000000 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexTest.kt +++ /dev/null @@ -1,123 +0,0 @@ -package org.oppia.android.util.math - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.Test -import org.junit.runner.RunWith -import org.oppia.android.app.model.MathEquation -import org.oppia.android.app.model.MathExpression -import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat -import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat -import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode -import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult -import org.robolectric.annotation.LooperMode - -/** Tests for [MathExpressionParser]. */ -@RunWith(AndroidJUnit4::class) -@LooperMode(LooperMode.Mode.PAUSED) -class ExpressionToLatexTest { - @Test - fun testLatex() { - // TODO: split up & move to separate test suites. Finish test cases. - - val exp1 = parseNumericExpressionWithAllErrors("1") - assertThat(exp1).convertsToLatexStringThat().isEqualTo("1") - - val exp2 = parseNumericExpressionWithAllErrors("1+2") - assertThat(exp2).convertsToLatexStringThat().isEqualTo("1 + 2") - - val exp3 = parseNumericExpressionWithAllErrors("1*2") - assertThat(exp3).convertsToLatexStringThat().isEqualTo("1 \\times 2") - - val exp4 = parseNumericExpressionWithAllErrors("1/2") - assertThat(exp4).convertsToLatexStringThat().isEqualTo("1 \\div 2") - - val exp5 = parseNumericExpressionWithAllErrors("1/2") - assertThat(exp5).convertsWithFractionsToLatexStringThat().isEqualTo("\\frac{1}{2}") - - val exp10 = parseNumericExpressionWithAllErrors("√2") - assertThat(exp10).convertsToLatexStringThat().isEqualTo("\\sqrt{2}") - - val exp11 = parseNumericExpressionWithAllErrors("√(1/2)") - assertThat(exp11).convertsToLatexStringThat().isEqualTo("\\sqrt{(1 \\div 2)}") - - val exp6 = parseAlgebraicExpressionWithAllErrors("x+y") - assertThat(exp6).convertsToLatexStringThat().isEqualTo("x + y") - - val exp7 = parseAlgebraicExpressionWithoutOptionalErrors("x^(1/y)") - assertThat(exp7).convertsToLatexStringThat().isEqualTo("x ^ {(1 \\div y)}") - - val exp8 = parseAlgebraicExpressionWithoutOptionalErrors("x^(1/y)") - assertThat(exp8).convertsWithFractionsToLatexStringThat().isEqualTo("x ^ {(\\frac{1}{y})}") - - val exp9 = parseAlgebraicExpressionWithoutOptionalErrors("x^y^z") - assertThat(exp9).convertsWithFractionsToLatexStringThat().isEqualTo("x ^ {y ^ {z}}") - - val eq1 = - parseAlgebraicEquationWithAllErrors( - "7a^2+b^2+c^2=0", allowedVariables = listOf("a", "b", "c") - ) - assertThat(eq1).convertsToLatexStringThat().isEqualTo("7a ^ {2} + b ^ {2} + c ^ {2} = 0") - - val eq2 = parseAlgebraicEquationWithAllErrors("sqrt(1+x)/x=1") - assertThat(eq2).convertsToLatexStringThat().isEqualTo("\\sqrt{1 + x} \\div x = 1") - - val eq3 = parseAlgebraicEquationWithAllErrors("sqrt(1+x)/x=1") - assertThat(eq3) - .convertsWithFractionsToLatexStringThat() - .isEqualTo("\\frac{\\sqrt{1 + x}}{x} = 1") - } - - private companion object { - // TODO: fix helper API. - - private fun parseNumericExpressionWithAllErrors(expression: String): MathExpression { - val result = - MathExpressionParser.parseNumericExpression(expression, ErrorCheckingMode.ALL_ERRORS) - return (result as MathParsingResult.Success).result - } - - private fun parseAlgebraicExpressionWithoutOptionalErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathExpression { - val result = - parseAlgebraicExpressionInternal( - expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables - ) - return (result as MathParsingResult.Success).result - } - - private fun parseAlgebraicExpressionWithAllErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathExpression { - val result = - parseAlgebraicExpressionInternal( - expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables - ) - return (result as MathParsingResult.Success).result - } - - private fun parseAlgebraicExpressionInternal( - expression: String, - errorCheckingMode: ErrorCheckingMode, - allowedVariables: List = listOf("x", "y", "z") - ): MathParsingResult { - return MathExpressionParser.parseAlgebraicExpression( - expression, allowedVariables, errorCheckingMode - ) - } - - private fun parseAlgebraicEquationWithAllErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathEquation { - val result = - MathExpressionParser.parseAlgebraicEquation( - expression, allowedVariables, - ErrorCheckingMode.ALL_ERRORS - ) - return (result as MathParsingResult.Success).result - } - } -} diff --git a/utility/src/test/java/org/oppia/android/util/math/FractionExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/FractionExtensionsTest.kt index 48121da69d7..3913e7b6dda 100644 --- a/utility/src/test/java/org/oppia/android/util/math/FractionExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/FractionExtensionsTest.kt @@ -26,6 +26,17 @@ class FractionExtensionsTest { denominator = 1 }.build() + private val ONE_FRACTION = Fraction.newBuilder().apply { + wholeNumber = 1 + denominator = 1 + }.build() + + private val NEGATIVE_ONE_FRACTION = Fraction.newBuilder().apply { + isNegative = true + wholeNumber = 1 + denominator = 1 + }.build() + private val ONE_HALF_FRACTION = Fraction.newBuilder().apply { numerator = 1 denominator = 2 @@ -37,6 +48,17 @@ class FractionExtensionsTest { denominator = 2 }.build() + private val ONE_THIRD_FRACTION = Fraction.newBuilder().apply { + numerator = 1 + denominator = 3 + }.build() + + private val NEGATIVE_ONE_THIRD_FRACTION = Fraction.newBuilder().apply { + isNegative = true + numerator = 1 + denominator = 3 + }.build() + private val ONE_AND_ONE_HALF_FRACTION = Fraction.newBuilder().apply { wholeNumber = 1 numerator = 1 @@ -183,6 +205,57 @@ class FractionExtensionsTest { assertThat(result).isTrue() } + @Test + fun testToWholeNumber_zeroFraction_returnsZero() { + val result = ZERO_FRACTION.toWholeNumber() + + assertThat(result).isEqualTo(0) + } + + @Test + fun testToWholeNumber_negativeZeroFraction_returnsZero() { + val result = NEGATIVE_ZERO_FRACTION.toWholeNumber() + + assertThat(result).isEqualTo(0) + } + + @Test + fun testToWholeNumber_two_returnsTwo() { + val result = TWO_FRACTION.toWholeNumber() + + assertThat(result).isEqualTo(2) + } + + @Test + fun testToWholeNumber_negativeTwo_returnsNegativeTwo() { + val result = NEGATIVE_TWO_FRACTION.toWholeNumber() + + assertThat(result).isEqualTo(-2) + } + + @Test + fun testToWholeNumber_oneHalf_returnsZero() { + val result = ONE_HALF_FRACTION.toWholeNumber() + + assertThat(result).isEqualTo(0) + } + + @Test + fun testToWholeNumber_oneAndOneHalf_returnsOne() { + val result = ONE_AND_ONE_HALF_FRACTION.toWholeNumber() + + assertThat(result).isEqualTo(1) + } + + @Test + fun testToWholeNumber_threeOnes_returnsZero() { + val result = THREE_ONES_FRACTION.toWholeNumber() + + // Even though the fraction is technically equivalent to '3', it being in improper form results + // in there not technically being a whole number component. + assertThat(result).isEqualTo(0) + } + @Test fun testToDouble_zeroFraction_returnsZero() { val result = ZERO_FRACTION.toDouble() @@ -374,6 +447,80 @@ class FractionExtensionsTest { assertThrows(ArithmeticException::class) { zeroDenominatorFraction.toSimplestForm() } } + @Test + fun testToProperForm_zeroFraction_returnsZero() { + val result = ZERO_FRACTION.toProperForm() + + assertThat(result).isEqualTo(ZERO_FRACTION) + } + + @Test + fun testToProperForm_two_returnsTwo() { + val result = TWO_FRACTION.toProperForm() + + assertThat(result).isEqualTo(TWO_FRACTION) + } + + @Test + fun testToProperForm_threeOnes_returnsThree() { + val result = THREE_ONES_FRACTION.toProperForm() + + // Correctly extract the '3' numerator to being a whole number. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(3) + } + + @Test + fun testToProperForm_oneHalf_returnsOneHalf() { + val result = ONE_HALF_FRACTION.toProperForm() + + assertThat(result).isEqualTo(ONE_HALF_FRACTION) + } + + @Test + fun testToProperForm_oneAndOneHalf_returnsOneAndOneHalf() { + val result = ONE_AND_ONE_HALF_FRACTION.toProperForm() + + // 1 1/2 is already in proper form. + assertThat(result).isEqualTo(ONE_AND_ONE_HALF_FRACTION) + } + + @Test + fun testToProperForm_threeHalves_returnsOneAndOneHalf() { + val result = THREE_HALVES_FRACTION.toProperForm() + + // 3/2 -> 1 1/2. + assertThat(result).isEqualTo(ONE_AND_ONE_HALF_FRACTION) + } + + @Test + fun testToProperForm_largeNegativeImproperFraction_reducesToSimplestProperFraction() { + val largeImproperFraction = Fraction.newBuilder().apply { + isNegative = true + numerator = 1650 + denominator = 209 + }.build() + + val result = largeImproperFraction.toProperForm() + + // Unlike toSimplestForm, toProperForm also extracts a whole number after reducing to the + // simplest denominator. + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(17) + assertThat(result).hasDenominatorThat().isEqualTo(19) + assertThat(result).hasWholeNumberThat().isEqualTo(7) + } + + @Test + fun testToProperForm_zeroDenominator_throwsException() { + val zeroDenominatorFraction = Fraction.getDefaultInstance() + + // Converting to simplest form results in a divide by zero in this case. + assertThrows(ArithmeticException::class) { zeroDenominatorFraction.toProperForm() } + } + @Test fun testToImproperForm_zero_returnsZeroFraction() { val result = ZERO_FRACTION.toImproperForm() @@ -527,4 +674,744 @@ class FractionExtensionsTest { assertThat(result).hasDenominatorThat().isEqualTo(2) assertThat(result).hasWholeNumberThat().isEqualTo(1) } + + @Test + fun testPlus_zeroAndZero_returnsZero() { + val lhsFraction = ZERO_FRACTION + val rhsFraction = ZERO_FRACTION + + val result = lhsFraction + rhsFraction + + assertThat(result).isEqualTo(ZERO_FRACTION) + } + + @Test + fun testPlus_oneAndZero_returnsOne() { + val lhsFraction = ZERO_FRACTION + val rhsFraction = ONE_FRACTION + + val result = lhsFraction + rhsFraction + + assertThat(result).isEqualTo(ONE_FRACTION) + } + + @Test + fun testPlus_oneHalfAndOneHalf_returnsOne() { + val lhsFraction = ONE_HALF_FRACTION + val rhsFraction = ONE_HALF_FRACTION + + val result = lhsFraction + rhsFraction + + assertThat(result).isEqualTo(ONE_FRACTION) + } + + @Test + fun testPlus_oneHalfAndNegativeOneHalf_returnsZero() { + val lhsFraction = ONE_HALF_FRACTION + val rhsFraction = NEGATIVE_ONE_HALF_FRACTION + + val result = lhsFraction + rhsFraction + + assertThat(result).isEqualTo(ZERO_FRACTION) + } + + @Test + fun testPlus_oneThirdAndOneHalf_returnsFiveSixths() { + val lhsFraction = ONE_THIRD_FRACTION + val rhsFraction = ONE_HALF_FRACTION + + val result = lhsFraction + rhsFraction + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(5) + assertThat(result).hasDenominatorThat().isEqualTo(6) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testPlus_oneHalfAndOneThird_returnsFiveSixths() { + val lhsFraction = ONE_HALF_FRACTION + val rhsFraction = ONE_THIRD_FRACTION + + val result = lhsFraction + rhsFraction + + // Demonstrate commutativity, i.e.: a+b=b+a. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(5) + assertThat(result).hasDenominatorThat().isEqualTo(6) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testPlus_twentyFiveThirtiethsAndFiveSevenths_returnsOneAndTwentyThreeFortyTwos() { + val lhsFraction = Fraction.newBuilder().apply { + numerator = 25 + denominator = 30 + }.build() + val rhsFraction = Fraction.newBuilder().apply { + numerator = 5 + denominator = 7 + }.build() + + val result = lhsFraction + rhsFraction + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(23) + assertThat(result).hasDenominatorThat().isEqualTo(42) + assertThat(result).hasWholeNumberThat().isEqualTo(1) + } + + @Test + fun testPlus_negativeOneAndOneThird_returnsNegativeTwoThirds() { + val lhsFraction = NEGATIVE_ONE_FRACTION + val rhsFraction = ONE_THIRD_FRACTION + + val result = lhsFraction + rhsFraction + + // Effectively subtracting fractions via addition should work as expected. + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(2) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testPlus_oneAndNegativeOneThird_returnsTwoThirds() { + val lhsFraction = ONE_FRACTION + val rhsFraction = NEGATIVE_ONE_THIRD_FRACTION + + val result = lhsFraction + rhsFraction + + // Effectively subtracting fractions via addition should work as expected. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(2) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testPlus_negativeOneAndNegativeOneThird_returnsNegativeOneAndOneThird() { + val lhsFraction = NEGATIVE_ONE_FRACTION + val rhsFraction = NEGATIVE_ONE_THIRD_FRACTION + + val result = lhsFraction + rhsFraction + + // Negative addition should work as expected. + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(1) + } + + @Test + fun testMinus_zeroAndZero_returnsZero() { + val lhsFraction = ZERO_FRACTION + val rhsFraction = ZERO_FRACTION + + val result = lhsFraction - rhsFraction + + assertThat(result).isEqualTo(ZERO_FRACTION) + } + + @Test + fun testMinus_oneAndZero_returnsOne() { + val lhsFraction = ONE_FRACTION + val rhsFraction = ZERO_FRACTION + + val result = lhsFraction - rhsFraction + + assertThat(result).isEqualTo(ONE_FRACTION) + } + + @Test + fun testMinus_oneHalfAndOneHalf_returnsZero() { + val lhsFraction = ONE_HALF_FRACTION + val rhsFraction = ONE_HALF_FRACTION + + val result = lhsFraction - rhsFraction + + assertThat(result).isEqualTo(ZERO_FRACTION) + } + + @Test + fun testMinus_oneHalfAndNegativeOneHalf_returnsOne() { + val lhsFraction = ONE_HALF_FRACTION + val rhsFraction = NEGATIVE_ONE_HALF_FRACTION + + val result = lhsFraction - rhsFraction + + // Minus a negative fraction should turn into regular addition. + assertThat(result).isEqualTo(ONE_FRACTION) + } + + @Test + fun testMinus_oneThirdAndOneHalf_returnsNegativeOneSixth() { + val lhsFraction = ONE_THIRD_FRACTION + val rhsFraction = ONE_HALF_FRACTION + + val result = lhsFraction - rhsFraction + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(6) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testMinus_oneHalfAndOneThird_returnsOneSixth() { + val lhsFraction = ONE_HALF_FRACTION + val rhsFraction = ONE_THIRD_FRACTION + + val result = lhsFraction - rhsFraction + + // Demonstrate anticommutativity, i.e.: a-b=-(b-a). + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(6) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testMinus_twentyFiveThirtiethsAndTwentyThreeSevenths_returnsNegTwoAndNineteenFortyTwos() { + val lhsFraction = Fraction.newBuilder().apply { + numerator = 25 + denominator = 30 + }.build() + val rhsFraction = Fraction.newBuilder().apply { + numerator = 23 + denominator = 7 + }.build() + + val result = lhsFraction - rhsFraction + + // Verify that the result of subtraction results in a properly formed fraction. + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(19) + assertThat(result).hasDenominatorThat().isEqualTo(42) + assertThat(result).hasWholeNumberThat().isEqualTo(2) + } + + @Test + fun testMinus_negativeOneAndOneThird_returnsNegativeOneAndOneThird() { + val lhsFraction = NEGATIVE_ONE_FRACTION + val rhsFraction = ONE_THIRD_FRACTION + + val result = lhsFraction - rhsFraction + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(1) + } + + @Test + fun testMinus_oneAndNegativeOneThird_returnsOneAndOneThird() { + val lhsFraction = ONE_FRACTION + val rhsFraction = NEGATIVE_ONE_THIRD_FRACTION + + val result = lhsFraction - rhsFraction + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(1) + } + + @Test + fun testMinus_negativeOneAndNegativeOneThird_returnsNegativeTwoThirds() { + val lhsFraction = NEGATIVE_ONE_FRACTION + val rhsFraction = NEGATIVE_ONE_THIRD_FRACTION + + val result = lhsFraction - rhsFraction + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(2) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testTimes_zeroAndZero_returnsZero() { + val lhsFraction = ZERO_FRACTION + val rhsFraction = ZERO_FRACTION + + val result = lhsFraction * rhsFraction + + assertThat(result).isEqualTo(ZERO_FRACTION) + } + + @Test + fun testTimes_oneAndZero_returnsZero() { + val lhsFraction = ONE_FRACTION + val rhsFraction = ZERO_FRACTION + + val result = lhsFraction * rhsFraction + + assertThat(result).isEqualTo(ZERO_FRACTION) + } + + @Test + fun testTimes_oneAndOne_returnsOne() { + val lhsFraction = ONE_FRACTION + val rhsFraction = ONE_FRACTION + + val result = lhsFraction * rhsFraction + + assertThat(result).isEqualTo(ONE_FRACTION) + } + + @Test + fun testTimes_twoAndOne_returnsTwo() { + val lhsFraction = TWO_FRACTION + val rhsFraction = ONE_FRACTION + + val result = lhsFraction * rhsFraction + + assertThat(result).isEqualTo(TWO_FRACTION) + } + + @Test + fun testTimes_oneHalfAndOneThird_returnsOneSixth() { + val lhsFraction = ONE_HALF_FRACTION + val rhsFraction = ONE_THIRD_FRACTION + + val result = lhsFraction * rhsFraction + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(6) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testTimes_oneThirdAndOneHalf_returnsOneSixth() { + val lhsFraction = ONE_THIRD_FRACTION + val rhsFraction = ONE_HALF_FRACTION + + val result = lhsFraction * rhsFraction + + // Demonstrate commutativity, i.e.: a*b=b*a. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(6) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testTimes_sevenHalvesAndTwentyFifteenths_returnsFourAndTwoThirds() { + val lhsFraction = Fraction.newBuilder().apply { + numerator = 7 + denominator = 2 + }.build() + val rhsFraction = Fraction.newBuilder().apply { + numerator = 20 + denominator = 15 + }.build() + + val result = lhsFraction * rhsFraction + + // Demonstrate that the multiplied result is a fully properly form fraction. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(2) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(4) + } + + @Test + fun testTimes_negativeTwoAndOneThird_returnsNegativeTwoThirds() { + val lhsFraction = NEGATIVE_TWO_FRACTION + val rhsFraction = ONE_THIRD_FRACTION + + val result = lhsFraction * rhsFraction + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(2) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testTimes_twoAndNegativeOneThird_returnsNegativeTwoThirds() { + val lhsFraction = TWO_FRACTION + val rhsFraction = NEGATIVE_ONE_THIRD_FRACTION + + val result = lhsFraction * rhsFraction + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(2) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testTimes_negativeTwoAndNegativeOneThird_returnsTwoThirds() { + val lhsFraction = NEGATIVE_TWO_FRACTION + val rhsFraction = NEGATIVE_ONE_THIRD_FRACTION + + val result = lhsFraction * rhsFraction + + // The negatives cancel out during multiplication. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(2) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testDivides_zeroAndZero_throwsException() { + val lhsFraction = ZERO_FRACTION + val rhsFraction = ZERO_FRACTION + + assertThrows(Exception::class) { lhsFraction / rhsFraction } + } + + @Test + fun testDivides_oneAndZero_throwsException() { + val lhsFraction = ONE_FRACTION + val rhsFraction = ZERO_FRACTION + + assertThrows(Exception::class) { lhsFraction / rhsFraction } + } + + @Test + fun testDivides_twoAndZero_throwsException() { + val lhsFraction = TWO_FRACTION + val rhsFraction = ZERO_FRACTION + + assertThrows(Exception::class) { lhsFraction / rhsFraction } + } + + @Test + fun testDivides_twoAndOne_returnsTwo() { + val lhsFraction = TWO_FRACTION + val rhsFraction = ONE_FRACTION + + val result = lhsFraction / rhsFraction + + assertThat(result).isEqualTo(TWO_FRACTION) + } + + @Test + fun testDivides_oneHalfAndOneThird_returnsOneAndOneHalf() { + val lhsFraction = ONE_HALF_FRACTION + val rhsFraction = ONE_THIRD_FRACTION + + val result = lhsFraction / rhsFraction + + // (1/2)/(1/3)=3/2=1 1/2. + assertThat(result).isEqualTo(ONE_AND_ONE_HALF_FRACTION) + } + + @Test + fun testDivides_oneThirdAndOneHalf_returnsTwoThirds() { + val lhsFraction = ONE_THIRD_FRACTION + val rhsFraction = ONE_HALF_FRACTION + + val result = lhsFraction / rhsFraction + + // Demonstrate anticommutativity, i.e.: a/b=1/(b/a). + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(2) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testDivides_fourThirdsAndTenThirtyFifths_returnsFourAndTwoThirds() { + val lhsFraction = Fraction.newBuilder().apply { + numerator = 4 + denominator = 3 + }.build() + val rhsFraction = Fraction.newBuilder().apply { + numerator = 10 + denominator = 35 + }.build() + + val result = lhsFraction / rhsFraction + + // Demonstrate that the divided result is a fully properly form fraction. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(2) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(4) + } + + @Test + fun testDivides_negativeTwoAndOneThird_returnsNegativeSix() { + val lhsFraction = NEGATIVE_TWO_FRACTION + val rhsFraction = ONE_THIRD_FRACTION + + val result = lhsFraction / rhsFraction + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(6) + } + + @Test + fun testDivides_twoAndNegativeOneThird_returnsNegativeSix() { + val lhsFraction = TWO_FRACTION + val rhsFraction = NEGATIVE_ONE_THIRD_FRACTION + + val result = lhsFraction / rhsFraction + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(6) + } + + @Test + fun testDivides_negativeTwoAndNegativeOneThird_returnsSix() { + val lhsFraction = NEGATIVE_TWO_FRACTION + val rhsFraction = NEGATIVE_ONE_THIRD_FRACTION + + val result = lhsFraction / rhsFraction + + // The negatives cancel out during division. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(6) + } + + @Test + fun testPow_zeroToZero_returnsOne() { + val fraction = ZERO_FRACTION + + val result = fraction pow 0 + + // See pow's documentation for specifics. + assertThat(result).isEqualTo(ONE_FRACTION) + } + + @Test + fun testPow_oneToZero_returnsOne() { + val fraction = ONE_FRACTION + + val result = fraction pow 0 + + assertThat(result).isEqualTo(ONE_FRACTION) + } + + @Test + fun testPow_oneToOne_returnsOne() { + val fraction = ONE_FRACTION + + val result = fraction pow 1 + + assertThat(result).isEqualTo(ONE_FRACTION) + } + + @Test + fun testPow_twoToZero_returnsOne() { + val fraction = TWO_FRACTION + + val result = fraction pow 0 + + assertThat(result).isEqualTo(ONE_FRACTION) + } + + @Test + fun testPow_twoToOne_returnsTwo() { + val fraction = TWO_FRACTION + + val result = fraction pow 1 + + assertThat(result).isEqualTo(TWO_FRACTION) + } + + @Test + fun testPow_twoToTwo_returnsFour() { + val fraction = TWO_FRACTION + + val result = fraction pow 2 + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(4) + } + + @Test + fun testPow_oneThirdToTwo_returnsOneNinth() { + val fraction = ONE_THIRD_FRACTION + + val result = fraction pow 2 + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(9) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testPow_negativeOneThirdToTwo_returnsOneNinth() { + val fraction = NEGATIVE_ONE_THIRD_FRACTION + + val result = fraction pow 2 + + // The negative sign is lost since the power is even. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(9) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testPow_negativeOneThirdToThree_returnsNegativeOneTwentySeventh() { + val fraction = NEGATIVE_ONE_THIRD_FRACTION + + val result = fraction pow 3 + + // The negative sign is preserved since the power is odd. + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(27) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testPow_twoToNegativeTwo_returnsOneFourth() { + val fraction = TWO_FRACTION + + val result = fraction pow -2 + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(4) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testPow_oneThirdToNegativeThree_returnsTwentySeven() { + val fraction = ONE_THIRD_FRACTION + + val result = fraction pow -3 + + // The negative sign is preserved since the power is odd. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(27) + } + + @Test + fun testPow_negativeOneThirdToNegativeTwo_returnsNine() { + val fraction = NEGATIVE_ONE_THIRD_FRACTION + + val result = fraction pow -2 + + // The negative sign is lost since the power is even. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(9) + } + + @Test + fun testPow_negativeOneThirdToNegativeThree_returnsNegativeTwentySeven() { + val fraction = NEGATIVE_ONE_THIRD_FRACTION + + val result = fraction pow -3 + + // The negative sign is preserved since the power is odd. + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(27) + } + + @Test + fun testPow_fourSeventhsCubed_returnsSixtyFourThreeHundredFortyThirds() { + val fraction = Fraction.newBuilder().apply { + numerator = 4 + denominator = 7 + }.build() + + val result = fraction pow 3 + + // Verify that the numerator is also correctly multiplied. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(64) + assertThat(result).hasDenominatorThat().isEqualTo(343) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testPow_twentyOneTwelfthsToNegativeThree_returnsFiveAndTwentyThreeSixtyFourths() { + val fraction = Fraction.newBuilder().apply { + numerator = 12 + denominator = 21 + }.build() + + val result = fraction pow -3 + + // Verify that the resulting value is in fully proper form. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(23) + assertThat(result).hasDenominatorThat().isEqualTo(64) + assertThat(result).hasWholeNumberThat().isEqualTo(5) + } + + @Test + fun testToWholeNumberFraction_zero_returnsZeroFraction() { + val wholeNumber = 0 + + val fraction = wholeNumber.toWholeNumberFraction() + + assertThat(fraction).isEqualTo(ZERO_FRACTION) + } + + @Test + fun testToWholeNumberFraction_one_returnsOneFraction() { + val wholeNumber = 1 + + val fraction = wholeNumber.toWholeNumberFraction() + + assertThat(fraction).isEqualTo(ONE_FRACTION) + } + + @Test + fun testToWholeNumberFraction_twentyThree_returnsTwentyThreeFraction() { + val wholeNumber = 23 + + val fraction = wholeNumber.toWholeNumberFraction() + + assertThat(fraction).hasNegativePropertyThat().isFalse() + assertThat(fraction).hasNumeratorThat().isEqualTo(0) + assertThat(fraction).hasDenominatorThat().isEqualTo(1) + assertThat(fraction).hasWholeNumberThat().isEqualTo(23) + } + + @Test + fun testToWholeNumberFraction_negativeZero_returnsZeroFraction() { + val wholeNumber = -0 + + val fraction = wholeNumber.toWholeNumberFraction() + + assertThat(fraction).isEqualTo(ZERO_FRACTION) + } + + @Test + fun testToWholeNumberFraction_negativeOne_returnsNegativeOneFraction() { + val wholeNumber = -1 + + val fraction = wholeNumber.toWholeNumberFraction() + + assertThat(fraction).isEqualTo(NEGATIVE_ONE_FRACTION) + } + + @Test + fun testToWholeNumberFraction_negativeTwentyThree_returnsNegativeTwentyThreeFraction() { + val wholeNumber = -23 + + val fraction = wholeNumber.toWholeNumberFraction() + + assertThat(fraction).hasNegativePropertyThat().isTrue() + assertThat(fraction).hasNumeratorThat().isEqualTo(0) + assertThat(fraction).hasDenominatorThat().isEqualTo(1) + assertThat(fraction).hasWholeNumberThat().isEqualTo(23) + } } diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt new file mode 100644 index 00000000000..0a81df14c54 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt @@ -0,0 +1,97 @@ +package org.oppia.android.util.math + +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.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.testing.math.RealSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression +import org.oppia.android.util.math.MathExpressionParser.Companion.parseNumericExpression +import org.robolectric.annotation.LooperMode + +/** + * Tests for [MathExpression] and [MathEquation] extensions. + * + * Note that this suite only verifies that the extensions work at a high-level. More specific + * verifications for operations like LaTeX conversion and expression evaluation are part of more + * targeted test suites such as [ExpressionToLatexConverterTest] and + * [NumericExpressionEvaluatorTest]. + */ +// FunctionName: test names are conventionally named with underscores. +// SameParameterValue: tests should have specific context included/excluded for readability. +@Suppress("FunctionName", "SameParameterValue") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class MathExpressionExtensionsTest { + @Test + fun testToRawLatex_algebraicExpression_divNotAsFraction_returnsLatexStringWithDivision() { + val expression = parseAlgebraicExpression("(x^2+7x-y)/2") + + val latex = expression.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("(x ^ {2} + 7x - y) \\div 2") + } + + @Test + fun testToRawLatex_algebraicExpression_divAsFraction_returnsLatexStringWithFraction() { + val expression = parseAlgebraicExpression("(x^2+7x-y)/2") + + val latex = expression.toRawLatex(divAsFraction = true) + + assertThat(latex).isEqualTo("\\frac{(x ^ {2} + 7x - y)}{2}") + } + + @Test + fun testToRawLatex_algebraicEquation_divNotAsFraction_returnsLatexStringWithDivisions() { + val equation = parseAlgebraicEquation("y/2=(x^2+x-7)/(2x)") + + val latex = equation.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("y \\div 2 = (x ^ {2} + x - 7) \\div (2x)") + } + + @Test + fun testToRawLatex_algebraicEquation_divAsFraction_returnsLatexStringWithFractions() { + val equation = parseAlgebraicEquation("y/2=(x^2+x-7)/(2x)") + + val latex = equation.toRawLatex(divAsFraction = true) + + assertThat(latex).isEqualTo("\\frac{y}{2} = \\frac{(x ^ {2} + x - 7)}{(2x)}") + } + + @Test + fun testEvaluateAsNumericExpression_numericExpression_returnsCorrectValue() { + val expression = parseNumericExpression("7*(3.14/0.76+8.4)^(3.8+1/(2+2/(7.4+1)))") + + val result = expression.evaluateAsNumericExpression() + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(322194.700361352) + } + + private companion object { + private fun parseNumericExpression(expression: String): MathExpression { + return parseNumericExpression(expression, ALL_ERRORS).retrieveExpectedSuccessfulResult() + } + + private fun parseAlgebraicExpression(expression: String): MathExpression { + return parseAlgebraicExpression( + expression, allowedVariables = listOf("x", "y", "z"), ALL_ERRORS + ).retrieveExpectedSuccessfulResult() + } + + private fun parseAlgebraicEquation(expression: String): MathEquation { + return MathExpressionParser.parseAlgebraicEquation( + expression, allowedVariables = listOf("x", "y", "z"), ALL_ERRORS + ).retrieveExpectedSuccessfulResult() + } + + private fun MathParsingResult.retrieveExpectedSuccessfulResult(): T { + assertThat(this).isInstanceOf(MathParsingResult.Success::class.java) + return (this as MathParsingResult.Success).result + } + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionEvaluatorTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionEvaluatorTest.kt new file mode 100644 index 00000000000..a16afda5a0e --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionEvaluatorTest.kt @@ -0,0 +1,239 @@ +package org.oppia.android.util.math + +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.app.model.MathExpression +import org.oppia.android.testing.math.RealSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.REQUIRED_ONLY +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression +import org.oppia.android.util.math.NumericExpressionEvaluator.Companion.evaluate +import org.robolectric.annotation.LooperMode + +/** + * Tests for [NumericExpressionEvaluator]. + * + * This test suite is primarily focused on verifying high-level behaviors of the evaluator. More + * specific tests exist for the sub-implementation pieces of the evaluator in [RealExtensionsTest] + * and [FractionExtensionsTest], and more complicated expression evaluation can be seen in + * [NumericExpressionParserTest]. + */ +// FunctionName: test names are conventionally named with underscores. +// SameParameterValue: tests should have specific context included/excluded for readability. +@Suppress("FunctionName", "SameParameterValue") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class NumericExpressionEvaluatorTest { + @Test + fun testEvaluate_defaultExpression_returnsNull() { + val expression = MathExpression.getDefaultInstance() + + val result = expression.evaluate() + + // Default expressions have nothing to evaluate. + assertThat(result).isNull() + } + + @Test + fun testEvaluate_constantExpression_returnsConstant() { + val expression = parseNumericExpression("2") + + val result = expression.evaluate() + + assertThat(result).isIntegerThat().isEqualTo(2) + } + + @Test + fun testEvaluate_variableExpression_returnsNull() { + val expression = parseAlgebraicExpression("2x") + + val result = expression.evaluate() + + // Cannot evaluate variables. + assertThat(result).isNull() + } + + @Test + fun testEvaluate_onePlusTwo_returnsThree() { + val expression = parseNumericExpression("1+2") + + val result = expression.evaluate() + + assertThat(result).isIntegerThat().isEqualTo(3) + } + + @Test + fun testEvaluate_oneMinusTwo_returnsNegativeOne() { + val expression = parseNumericExpression("1-2") + + val result = expression.evaluate() + + assertThat(result).isIntegerThat().isEqualTo(-1) + } + + @Test + fun testEvaluate_twoTimesSeven_returnsFourteen() { + val expression = parseNumericExpression("2*7") + + val result = expression.evaluate() + + assertThat(result).isIntegerThat().isEqualTo(14) + } + + @Test + fun testEvaluate_fourDividedByTwo_returnsTwo() { + val expression = parseNumericExpression("4/2") + + val result = expression.evaluate() + + assertThat(result).isIntegerThat().isEqualTo(2) + } + + @Test + fun testEvaluate_oneDividedByTwo_returnsOneHalfFraction() { + val expression = parseNumericExpression("1/2") + + val result = expression.evaluate() + + assertThat(result).isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + } + + @Test + fun testEvaluate_minusOne_returnsMinusOne() { + val expression = parseNumericExpression("-2") + + val result = expression.evaluate() + + assertThat(result).isIntegerThat().isEqualTo(-2) + } + + @Test + fun testEvaluate_plusTwo_returnsTwo() { + val expression = parseNumericExpression("+2", errorCheckingMode = REQUIRED_ONLY) + + val result = expression.evaluate() + + assertThat(result).isIntegerThat().isEqualTo(2) + } + + @Test + fun testEvaluate_minusGroupOneMinusTwo_returnsOne() { + val expression = parseNumericExpression("-(1-2)") + + val result = expression.evaluate() + + assertThat(result).isIntegerThat().isEqualTo(1) + } + + @Test + fun testEvaluate_plusGroupOneMinusTwo_returnsMinusOne() { + val expression = parseNumericExpression("+(1-2)", errorCheckingMode = REQUIRED_ONLY) + + val result = expression.evaluate() + + assertThat(result).isIntegerThat().isEqualTo(-1) + } + + @Test + fun testEvaluate_twoTimesNegativeSeven_returnsNegativeFourteen() { + val expression = parseNumericExpression("2*-7") + + val result = expression.evaluate() + + assertThat(result).isIntegerThat().isEqualTo(-14) + } + + @Test + fun testEvaluate_oneDividedByGroupOfOnePlusTwo_returnsOneThirdFraction() { + val expression = parseNumericExpression("1/(1+2)") + + val result = expression.evaluate() + + assertThat(result).isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(3) + } + } + + @Test + fun testEvaluate_twoRaisedToThree_returnsEight() { + val expression = parseNumericExpression("2^3") + + val result = expression.evaluate() + + assertThat(result).isIntegerThat().isEqualTo(8) + } + + @Test + fun testEvaluate_groupOneDividedByTwoRaisedToNegativeThree_returnsEightFraction() { + val expression = parseNumericExpression("1/(2^-3)") + + val result = expression.evaluate() + + assertThat(result).isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(8) + hasNumeratorThat().isEqualTo(0) + hasDenominatorThat().isEqualTo(1) + } + } + + @Test + fun testEvaluate_rootOfTwo_returnsSquareRootOfTwoDecimal() { + val expression = parseNumericExpression("√2") + + val result = expression.evaluate() + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(1.414213562) + } + + @Test + fun testEvaluate_rootOfGroupTwoRaisedToTwo_returnsTwoInteger() { + val expression = parseNumericExpression("√(2^2)") + + val result = expression.evaluate() + + assertThat(result).isIntegerThat().isEqualTo(2) + } + + @Test + fun testEvaluate_threeRaisedToOneDividedByTwo_returnsSquareRootOfThreeDecimal() { + val expression = parseNumericExpression("3^(1/2)") + + val result = expression.evaluate() + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(1.732050808) + } + + private companion object { + private fun parseNumericExpression( + expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + ): MathExpression { + return MathExpressionParser.parseNumericExpression( + expression, errorCheckingMode + ).retrieveExpectedSuccessfulResult() + } + + private fun parseAlgebraicExpression(expression: String): MathExpression { + return parseAlgebraicExpression( + expression, allowedVariables = listOf("x", "y", "z"), ALL_ERRORS + ).retrieveExpectedSuccessfulResult() + } + + private fun MathParsingResult.retrieveExpectedSuccessfulResult(): T { + assertThat(this).isInstanceOf(MathParsingResult.Success::class.java) + return (this as MathParsingResult.Success).result + } + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index afea2ad7908..bd236e905dc 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -10,7 +10,6 @@ import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode -import kotlin.math.sqrt /** * Tests for [MathExpressionParser]. @@ -20,6 +19,11 @@ import kotlin.math.sqrt * both operator associativity and precedence). This suite does not cover errors (see * [MathExpressionParserTest] for those tests), nor algebraic expressions (see * [AlgebraicExpressionParserTest]). + * + * Further, many of the tests also verify that the expression evaluates to the correct value. This + * suite's goal is not to test that the evaluator works functionally but, rather, that it works + * practically. There are targeted tests designed to fail for the evaluator if issues are + * introduced (see [NumericExpressionEvaluatorTest]). */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @@ -36,6 +40,7 @@ class NumericExpressionParserTest { withValueThat().isIntegerThat().isEqualTo(1) } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(1) } @Test @@ -47,6 +52,7 @@ class NumericExpressionParserTest { withValueThat().isIntegerThat().isEqualTo(2) } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(2) } @Test @@ -58,6 +64,7 @@ class NumericExpressionParserTest { withValueThat().isIntegerThat().isEqualTo(732) } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(732) } @Test @@ -69,6 +76,7 @@ class NumericExpressionParserTest { withValueThat().isIrrationalThat().isWithin(1e-5).of(2.5) } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(2.5) } @Test @@ -89,6 +97,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(3) } @Test @@ -109,6 +118,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(-1) } @Test @@ -129,6 +139,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(-1) } @Test @@ -149,6 +160,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(2) } @Test @@ -169,6 +181,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(2) } @Test @@ -189,6 +202,12 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } } @Test @@ -209,6 +228,12 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } } @Test @@ -229,6 +254,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(1) } @Test @@ -244,6 +270,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(-2) } @Test @@ -259,6 +286,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(2) } @Test @@ -272,6 +300,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(2) } @Test @@ -287,6 +316,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(1.414213562) } @Test @@ -302,6 +332,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(1.414213562) } @Test @@ -332,6 +363,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(7) } @Test @@ -362,6 +394,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(162) } @Test @@ -394,6 +427,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(1296) } @Test @@ -419,6 +453,9 @@ class NumericExpressionParserTest { } } } + // Note that this may differ from other calculators since the negation is applied last (others + // may interpret it as (-3)^4). + assertThat(expression).evaluatesToIntegerThat().isEqualTo(-81) } @Test @@ -444,6 +481,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(9.0) } @Test @@ -494,6 +532,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(-1) } @Test @@ -544,14 +583,20 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(1) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(4) + } } @Test fun testParse_nestedExponents_returnsExpWithExponentsAsRightAssociative() { - val expression = parseNumericExpressionWithoutOptionalErrors("2^3^4") + val expression = parseNumericExpressionWithoutOptionalErrors("2^3^1.5") // Exponentiation is resolved with right associativity, that is, from right to left. This is - // made clearer by grouping: 2^(3^4). Note that this is a specific choice made by the + // made clearer by grouping: 2^(3^1.5). Note that this is a specific choice made by the // implementation as there's no broad consensus around exponentiation associativity for infix // exponentiation. Right associativity is ideal since it more closely matches written-out // exponentiation (where the nested exponent is resolved first). @@ -571,18 +616,19 @@ class NumericExpressionParserTest { } rightOperand { constant { - withValueThat().isIntegerThat().isEqualTo(4) + withValueThat().isIrrationalThat().isWithin(1e-5).of(1.5) } } } } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(36.660445757) } @Test fun testParse_nestedExponents_withGroups_returnsExpWithForcedLeftAssociativeExponent() { - val expression = parseNumericExpressionWithAllErrors("(2^3)^4") + val expression = parseNumericExpressionWithAllErrors("(2^3)^1.5") // Nested exponentiation can be "forced" to be left-associative by using a group to explicitly // change the order (since groups have higher precedence than exponents). @@ -606,11 +652,12 @@ class NumericExpressionParserTest { } rightOperand { constant { - withValueThat().isIntegerThat().isEqualTo(4) + withValueThat().isIrrationalThat().isWithin(1e-5).of(1.5) } } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(22.627416998) } @Test @@ -651,6 +698,7 @@ class NumericExpressionParserTest { } } } + // Cannot evaluate this expression in real numbers. } @Test @@ -688,6 +736,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(-3) } @Test @@ -708,6 +757,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(6) } @Test @@ -732,6 +782,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(6) } @Test @@ -764,6 +815,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(6) } @Test @@ -807,6 +859,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(21) } @Test @@ -832,6 +885,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(3.464101615) } @Test @@ -861,6 +915,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(2.449489743) } @Test @@ -886,6 +941,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(2.828427125) } @Test @@ -915,6 +971,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0) } @Test @@ -943,6 +1000,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(4.242640687) } @Test @@ -985,6 +1043,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(4.898979486) } @Test @@ -1019,6 +1078,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(5.242640687) } @Test @@ -1078,6 +1138,12 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(73) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } } @Test @@ -1114,6 +1180,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(32) } @Test @@ -1155,6 +1222,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(8192) } @Test @@ -1256,6 +1324,12 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(351) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(3) + } } @Test @@ -1345,6 +1419,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(956.092045778) } @Test @@ -1376,6 +1451,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(6) } @Test @@ -1437,6 +1513,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(13) } @Test @@ -1556,6 +1633,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(322194.700361352) } @Test diff --git a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt index 2e13da959aa..2c0b7581c9e 100644 --- a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt @@ -1,29 +1,43 @@ package org.oppia.android.util.math -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.app.model.Fraction import org.oppia.android.app.model.Real import org.oppia.android.testing.assertThrows +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized import org.oppia.android.testing.math.RealSubject.Companion.assertThat +import org.oppia.android.util.math.FractionParser.Companion.parseFraction import org.robolectric.annotation.LooperMode /** Tests for [Real] extensions. */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") -@RunWith(AndroidJUnit4::class) +@RunWith(OppiaParameterizedTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class RealExtensionsTest { private companion object { private const val PI = 3.1415 + private val ZERO_FRACTION = Fraction.newBuilder().apply { + numerator = 0 + denominator = 1 + }.build() + private val ONE_HALF_FRACTION = Fraction.newBuilder().apply { numerator = 1 denominator = 2 }.build() + private val ONE_FOURTH_FRACTION = Fraction.newBuilder().apply { + numerator = 1 + denominator = 4 + }.build() + private val ONE_AND_ONE_HALF_FRACTION = Fraction.newBuilder().apply { numerator = 1 denominator = 2 @@ -43,6 +57,16 @@ class RealExtensionsTest { private val NEGATIVE_PI_REAL = createIrrationalReal(-PI) } + @Parameter var lhsInt: Int = Int.MIN_VALUE + @Parameter lateinit var lhsFrac: String + @Parameter var lhsDouble: Double = Double.MIN_VALUE + @Parameter var rhsInt: Int = Int.MIN_VALUE + @Parameter lateinit var rhsFrac: String + @Parameter var rhsDouble: Double = Double.MIN_VALUE + @Parameter var expInt: Int = Int.MIN_VALUE + @Parameter lateinit var expFrac: String + @Parameter var expDouble: Double = Double.MIN_VALUE + @Test fun testIsRational_default_returnsFalse() { val defaultReal = Real.getDefaultInstance() @@ -73,6 +97,36 @@ class RealExtensionsTest { assertThat(result).isFalse() } + @Test + fun testIsInteger_default_returnsFalse() { + val defaultReal = Real.getDefaultInstance() + + val result = defaultReal.isInteger() + + assertThat(result).isFalse() + } + + @Test + fun testIsInteger_twoInteger_returnsTrue() { + val result = TWO_REAL.isInteger() + + assertThat(result).isTrue() + } + + @Test + fun testIsInteger_oneHalfFraction_returnsFalse() { + val result = ONE_HALF_REAL.isInteger() + + assertThat(result).isFalse() + } + + @Test + fun testIsInteger_piIrrational_returnsFalse() { + val result = PI_REAL.isInteger() + + assertThat(result).isFalse() + } + @Test fun testIsNegative_default_throwsException() { val defaultReal = Real.getDefaultInstance() @@ -399,12 +453,1334 @@ class RealExtensionsTest { assertThat(result).isIrrationalThat().isWithin(1e-5).of(PI) } + + /* + * Begin operator tests. + * + * Note that parameterized tests are used here to reduce the length of the overall test despite it + * being not best practice (since each parameterized test is actually verifying multiple + * behaviors). + * + * For a reference on the iteration names: + * - 'identity' refers to an operator identity (i.e. a value which doesn't result in a change to + * the other operand of the operation) + * - commutativity refers to verifying commutativity, e.g.: a+b=b+a or a*b=b*a + * - noncommutativity refers to verifying that commutativity doesn't hold, e.g.: 2^3 != 3^2 + * - anticommutativity refers to verifying that commutativity is operationally reversed, e.g.: + * a-b=-(b-a) and a/b=1/(b/a). + */ + + // Addition tests. + + @Test + @RunParameterized( + Iteration("identity+identity", "lhsInt=0", "rhsInt=0", "expInt=0"), + Iteration("int+identity", "lhsInt=1", "rhsInt=0", "expInt=1"), + Iteration("int+int", "lhsInt=1", "rhsInt=2", "expInt=3"), + Iteration("commutativity", "lhsInt=2", "rhsInt=1", "expInt=3"), + Iteration("int+-int", "lhsInt=1", "rhsInt=-2", "expInt=-1"), + Iteration("-int+int", "lhsInt=-1", "rhsInt=2", "expInt=1"), + Iteration("-int+-int", "lhsInt=-1", "rhsInt=-2", "expInt=-3") + ) + fun testPlus_intAndInt_returnsInt() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal + rhsReal + + assertThat(result).isIntegerThat().isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("identity+identity", "lhsInt=0", "rhsFrac=0/1", "expFrac=0/1"), + Iteration("int+identity", "lhsInt=1", "rhsFrac=0/1", "expFrac=1"), + Iteration("int+fraction", "lhsInt=2", "rhsFrac=1/3", "expFrac=2 1/3"), + Iteration("int+wholeNumberFraction", "lhsInt=2", "rhsFrac=3/1", "expFrac=5"), + Iteration("commutativity", "lhsInt=3", "rhsFrac=2/1", "expFrac=5"), + Iteration("int+-fraction", "lhsInt=2", "rhsFrac=-1/3", "expFrac=1 2/3"), + Iteration("-int+fraction", "lhsInt=-2", "rhsFrac=1/3", "expFrac=-1 2/3"), + Iteration("-int+-fraction", "lhsInt=-2", "rhsFrac=-1/3", "expFrac=-2 1/3") + ) + fun testPlus_intAndFraction_returnsFraction() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal + rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity+identity", "lhsInt=0", "rhsDouble=0.0", "expDouble=0.0"), + Iteration("int+identity", "lhsInt=1", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("int+double", "lhsInt=1", "rhsDouble=3.14", "expDouble=4.14"), + Iteration("int+wholeNumberDouble", "lhsInt=1", "rhsDouble=3.0", "expDouble=4.0"), + Iteration("commutativity", "lhsInt=3", "rhsDouble=1.0", "expDouble=4.0"), + Iteration("int+-double", "lhsInt=1", "rhsDouble=-3.14", "expDouble=-2.14"), + Iteration("-int+double", "lhsInt=-1", "rhsDouble=3.14", "expDouble=2.14"), + Iteration("-int+-double", "lhsInt=-1", "rhsDouble=-3.14", "expDouble=-4.14") + ) + fun testPlus_intAndDouble_returnsDouble() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal + rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity+identity", "lhsFrac=0/1", "rhsInt=0", "expFrac=0/1"), + Iteration("fraction+identity", "lhsFrac=1/1", "rhsInt=0", "expFrac=1"), + Iteration("fraction+int", "lhsFrac=1/3", "rhsInt=2", "expFrac=2 1/3"), + Iteration("wholeNumberFraction+int", "lhsFrac=3/1", "rhsInt=2", "expFrac=5"), + Iteration("commutativity", "lhsFrac=2/1", "rhsInt=3", "expFrac=5"), + Iteration("fraction+-int", "lhsFrac=1/3", "rhsInt=-2", "expFrac=-1 2/3"), + Iteration("-fraction+int", "lhsFrac=-1/3", "rhsInt=2", "expFrac=1 2/3"), + Iteration("-fraction+-int", "lhsFrac=-1/3", "rhsInt=-2", "expFrac=-2 1/3") + ) + fun testPlus_fractionAndInt_returnsFraction() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal + rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity+identity", "lhsFrac=0/1", "rhsFrac=0/1", "expFrac=0/1"), + Iteration("fraction+identity", "lhsFrac=3/2", "rhsFrac=0/1", "expFrac=1 1/2"), + Iteration("fraction+fraction", "lhsFrac=3/2", "rhsFrac=1/3", "expFrac=1 5/6"), + Iteration("commutativity", "lhsFrac=1/3", "rhsFrac=3/2", "expFrac=1 5/6"), + Iteration("fraction+-fraction", "lhsFrac=1/2", "rhsFrac=-1/3", "expFrac=1/6"), + Iteration("-fraction+fraction", "lhsFrac=-1/2", "rhsFrac=1/3", "expFrac=-1/6"), + Iteration("-fraction+-fraction", "lhsFrac=-1/2", "rhsFrac=-1/3", "expFrac=-5/6") + ) + fun testPlus_fractionAndFraction_returnsFraction() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal + rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity+identity", "lhsFrac=0/1", "rhsDouble=0.0", "expDouble=0.0"), + Iteration("fraction+identity", "lhsFrac=3/2", "rhsDouble=0.0", "expDouble=1.5"), + Iteration("fraction+double", "lhsFrac=3/2", "rhsDouble=3.14", "expDouble=4.64"), + Iteration("wholeNumberFraction+double", "lhsFrac=3/1", "rhsDouble=2.0", "expDouble=5.0"), + Iteration("commutativity", "lhsFrac=2/1", "rhsDouble=3.0", "expDouble=5.0"), + Iteration("fraction+-double", "lhsFrac=3/2", "rhsDouble=-3.14", "expDouble=-1.64"), + Iteration("-fraction+double", "lhsFrac=-3/2", "rhsDouble=3.14", "expDouble=1.64"), + Iteration("-fraction+-double", "lhsFrac=-3/2", "rhsDouble=-3.14", "expDouble=-4.64") + ) + fun testPlus_fractionAndDouble_returnsDouble() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal + rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity+identity", "lhsDouble=0.0", "rhsInt=0", "expDouble=0.0"), + Iteration("double+identity", "lhsDouble=1.0", "rhsInt=0", "expDouble=1.0"), + Iteration("double+int", "lhsDouble=3.14", "rhsInt=1", "expDouble=4.14"), + Iteration("wholeNumberDouble+int", "lhsDouble=3.0", "rhsInt=1", "expDouble=4.0"), + Iteration("commutativity", "lhsDouble=1.0", "rhsInt=3", "expDouble=4.0"), + Iteration("double+-int", "lhsDouble=3.14", "rhsInt=-1", "expDouble=2.14"), + Iteration("-double+int", "lhsDouble=-3.14", "rhsInt=1", "expDouble=-2.14"), + Iteration("-double+-int", "lhsDouble=-3.14", "rhsInt=-1", "expDouble=-4.14") + ) + fun testPlus_doubleAndInt_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal + rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity+identity", "lhsDouble=0.0", "rhsFrac=0/1", "expDouble=0.0"), + Iteration("double+identity", "lhsDouble=3.14", "rhsFrac=0/1", "expDouble=3.14"), + Iteration("double+fraction", "lhsDouble=3.14", "rhsFrac=3/2", "expDouble=4.64"), + Iteration("double+wholeNumberFraction", "lhsDouble=2.0", "rhsFrac=3/1", "expDouble=5.0"), + Iteration("commutativity", "lhsDouble=3.0", "rhsFrac=2/1", "expDouble=5.0"), + Iteration("double+-fraction", "lhsDouble=3.14", "rhsFrac=-3/2", "expDouble=1.64"), + Iteration("-double+fraction", "lhsDouble=-3.14", "rhsFrac=3/2", "expDouble=-1.64"), + Iteration("-double+-fraction", "lhsDouble=-3.14", "rhsFrac=-3/2", "expDouble=-4.64") + ) + fun testPlus_doubleAndFraction_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal + rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity+identity", "lhsDouble=0.0", "rhsDouble=0.0", "expDouble=0.0"), + Iteration("double+identity", "lhsDouble=1.0", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("double+double", "lhsDouble=3.14", "rhsDouble=2.7", "expDouble=5.84"), + Iteration("commutativity", "lhsDouble=2.7", "rhsDouble=3.14", "expDouble=5.84"), + Iteration("double+-double", "lhsDouble=3.14", "rhsDouble=-2.7", "expDouble=0.44"), + Iteration("-double+double", "lhsDouble=-3.14", "rhsDouble=2.7", "expDouble=-0.44"), + Iteration("-double+-double", "lhsDouble=-3.14", "rhsDouble=-2.7", "expDouble=-5.84") + ) + fun testPlus_doubleAndDouble_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal + rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + // Subtraction tests. + + @Test + @RunParameterized( + Iteration("identity-identity", "lhsInt=0", "rhsInt=0", "expInt=0"), + Iteration("int-identity", "lhsInt=1", "rhsInt=0", "expInt=1"), + Iteration("int-int", "lhsInt=1", "rhsInt=2", "expInt=-1"), + Iteration("anticommutativity", "lhsInt=2", "rhsInt=1", "expInt=1"), + Iteration("int--int", "lhsInt=1", "rhsInt=-2", "expInt=3"), + Iteration("-int-int", "lhsInt=-1", "rhsInt=2", "expInt=-3"), + Iteration("-int--int", "lhsInt=-1", "rhsInt=-2", "expInt=1") + ) + fun testMinus_intAndInt_returnsInt() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal - rhsReal + + assertThat(result).isIntegerThat().isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("identity-identity", "lhsInt=0", "rhsFrac=0/1", "expFrac=0/1"), + Iteration("int-identity", "lhsInt=1", "rhsFrac=0/1", "expFrac=1"), + Iteration("int-fraction", "lhsInt=2", "rhsFrac=1/3", "expFrac=1 2/3"), + Iteration("int-wholeNumberFraction", "lhsInt=2", "rhsFrac=3/1", "expFrac=-1"), + Iteration("anticommutativity", "lhsInt=3", "rhsFrac=2/1", "expFrac=1"), + Iteration("int--fraction", "lhsInt=2", "rhsFrac=-1/3", "expFrac=2 1/3"), + Iteration("-int-fraction", "lhsInt=-2", "rhsFrac=1/3", "expFrac=-2 1/3"), + Iteration("-int--fraction", "lhsInt=-2", "rhsFrac=-1/3", "expFrac=-1 2/3") + ) + fun testMinus_intAndFraction_returnsFraction() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal - rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity-identity", "lhsInt=0", "rhsDouble=0.0", "expDouble=0.0"), + Iteration("int-identity", "lhsInt=1", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("int-double", "lhsInt=1", "rhsDouble=3.14", "expDouble=-2.14"), + Iteration("int-wholeNumberDouble", "lhsInt=1", "rhsDouble=3.0", "expDouble=-2.0"), + Iteration("anticommutativity", "lhsInt=3", "rhsDouble=1.0", "expDouble=2.0"), + Iteration("int--double", "lhsInt=1", "rhsDouble=-3.14", "expDouble=4.14"), + Iteration("-int-double", "lhsInt=-1", "rhsDouble=3.14", "expDouble=-4.14"), + Iteration("-int--double", "lhsInt=-1", "rhsDouble=-3.14", "expDouble=2.14") + ) + fun testMinus_intAndDouble_returnsDouble() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal - rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity-identity", "lhsFrac=0/1", "rhsInt=0", "expFrac=0/1"), + Iteration("fraction-identity", "lhsFrac=1/1", "rhsInt=0", "expFrac=1"), + Iteration("fraction-int", "lhsFrac=1/3", "rhsInt=2", "expFrac=-1 2/3"), + Iteration("wholeNumberFraction-int", "lhsFrac=3/1", "rhsInt=2", "expFrac=1"), + Iteration("anticommutativity", "lhsFrac=2/1", "rhsInt=3", "expFrac=-1"), + Iteration("fraction--int", "lhsFrac=1/3", "rhsInt=-2", "expFrac=2 1/3"), + Iteration("-fraction-int", "lhsFrac=-1/3", "rhsInt=2", "expFrac=-2 1/3"), + Iteration("-fraction--int", "lhsFrac=-1/3", "rhsInt=-2", "expFrac=1 2/3") + ) + fun testMinus_fractionAndInt_returnsFraction() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal - rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity-identity", "lhsFrac=0/1", "rhsFrac=0/1", "expFrac=0/1"), + Iteration("fraction-identity", "lhsFrac=3/2", "rhsFrac=0/1", "expFrac=1 1/2"), + Iteration("fraction-fraction", "lhsFrac=3/2", "rhsFrac=1/3", "expFrac=1 1/6"), + Iteration("anticommutativity", "lhsFrac=1/3", "rhsFrac=3/2", "expFrac=-1 1/6"), + Iteration("fraction--fraction", "lhsFrac=1/2", "rhsFrac=-1/3", "expFrac=5/6"), + Iteration("-fraction-fraction", "lhsFrac=-1/2", "rhsFrac=1/3", "expFrac=-5/6"), + Iteration("-fraction--fraction", "lhsFrac=-1/2", "rhsFrac=-1/3", "expFrac=-1/6") + ) + fun testMinus_fractionAndFraction_returnsFraction() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal - rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity-identity", "lhsFrac=0/1", "rhsDouble=0.0", "expDouble=0.0"), + Iteration("fraction-identity", "lhsFrac=3/2", "rhsDouble=0.0", "expDouble=1.5"), + Iteration("fraction-double", "lhsFrac=3/2", "rhsDouble=3.14", "expDouble=-1.64"), + Iteration("wholeNumberFraction-double", "lhsFrac=3/1", "rhsDouble=2.0", "expDouble=1.0"), + Iteration("anticommutativity", "lhsFrac=2/1", "rhsDouble=3.0", "expDouble=-1.0"), + Iteration("fraction--double", "lhsFrac=3/2", "rhsDouble=-3.14", "expDouble=4.64"), + Iteration("-fraction-double", "lhsFrac=-3/2", "rhsDouble=3.14", "expDouble=-4.64"), + Iteration("-fraction--double", "lhsFrac=-3/2", "rhsDouble=-3.14", "expDouble=1.64") + ) + fun testMinus_fractionAndDouble_returnsDouble() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal - rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity-identity", "lhsDouble=0.0", "rhsInt=0", "expDouble=0.0"), + Iteration("double-identity", "lhsDouble=1.0", "rhsInt=0", "expDouble=1.0"), + Iteration("double-int", "lhsDouble=3.14", "rhsInt=1", "expDouble=2.14"), + Iteration("wholeNumberDouble-int", "lhsDouble=3.0", "rhsInt=1", "expDouble=2.0"), + Iteration("anticommutativity", "lhsDouble=1.0", "rhsInt=3", "expDouble=-2.0"), + Iteration("double--int", "lhsDouble=3.14", "rhsInt=-1", "expDouble=4.14"), + Iteration("-double-int", "lhsDouble=-3.14", "rhsInt=1", "expDouble=-4.14"), + Iteration("-double--int", "lhsDouble=-3.14", "rhsInt=-1", "expDouble=-2.14") + ) + fun testMinus_doubleAndInt_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal - rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity-identity", "lhsDouble=0.0", "rhsFrac=0/1", "expDouble=0.0"), + Iteration("double-identity", "lhsDouble=3.14", "rhsFrac=0/1", "expDouble=3.14"), + Iteration("double-fraction", "lhsDouble=3.14", "rhsFrac=3/2", "expDouble=1.64"), + Iteration("double-wholeNumberFraction", "lhsDouble=2.0", "rhsFrac=3/1", "expDouble=-1.0"), + Iteration("anticommutativity", "lhsDouble=3.0", "rhsFrac=2/1", "expDouble=1.0"), + Iteration("double--fraction", "lhsDouble=3.14", "rhsFrac=-3/2", "expDouble=4.64"), + Iteration("-double-fraction", "lhsDouble=-3.14", "rhsFrac=3/2", "expDouble=-4.64"), + Iteration("-double--fraction", "lhsDouble=-3.14", "rhsFrac=-3/2", "expDouble=-1.64") + ) + fun testMinus_doubleAndFraction_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal - rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity-identity", "lhsDouble=0.0", "rhsDouble=0.0", "expDouble=0.0"), + Iteration("double-identity", "lhsDouble=1.0", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("double-double", "lhsDouble=3.14", "rhsDouble=2.7", "expDouble=0.44"), + Iteration("anticommutativity", "lhsDouble=2.7", "rhsDouble=3.14", "expDouble=-0.44"), + Iteration("double--double", "lhsDouble=3.14", "rhsDouble=-2.7", "expDouble=5.84"), + Iteration("-double-double", "lhsDouble=-3.14", "rhsDouble=2.7", "expDouble=-5.84"), + Iteration("-double--double", "lhsDouble=-3.14", "rhsDouble=-2.7", "expDouble=-0.44") + ) + fun testMinus_doubleAndDouble_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal - rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + // Multiplication tests. + + @Test + @RunParameterized( + Iteration("identity*identity", "lhsInt=1", "rhsInt=1", "expInt=1"), + Iteration("int*identity", "lhsInt=2", "rhsInt=1", "expInt=2"), + Iteration("int*int", "lhsInt=3", "rhsInt=2", "expInt=6"), + Iteration("commutativity", "lhsInt=2", "rhsInt=3", "expInt=6"), + Iteration("int*-int", "lhsInt=3", "rhsInt=-2", "expInt=-6"), + Iteration("-int*int", "lhsInt=-3", "rhsInt=2", "expInt=-6"), + Iteration("-int*-int", "lhsInt=-3", "rhsInt=-2", "expInt=6") + ) + fun testTimes_intAndInt_returnsInt() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal * rhsReal + + assertThat(result).isIntegerThat().isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("identity*identity", "lhsInt=1", "rhsFrac=1", "expFrac=1"), + Iteration("int*identity", "lhsInt=2", "rhsFrac=1", "expFrac=2"), + Iteration("int*fraction", "lhsInt=2", "rhsFrac=1/3", "expFrac=2/3"), + Iteration("int*wholeNumberFraction", "lhsInt=2", "rhsFrac=3/1", "expFrac=6"), + Iteration("commutativity", "lhsInt=3", "rhsFrac=2/1", "expFrac=6"), + Iteration("int*-fraction", "lhsInt=2", "rhsFrac=-1/3", "expFrac=-2/3"), + Iteration("-int*fraction", "lhsInt=-2", "rhsFrac=1/3", "expFrac=-2/3"), + Iteration("-int*-fraction", "lhsInt=-2", "rhsFrac=-1/3", "expFrac=2/3") + ) + fun testTimes_intAndFraction_returnsFraction() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal * rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity*identity", "lhsInt=1", "rhsDouble=1.0", "expDouble=1.0"), + Iteration("int*identity", "lhsInt=2", "rhsDouble=1.0", "expDouble=2.0"), + Iteration("int*double", "lhsInt=2", "rhsDouble=3.14", "expDouble=6.28"), + Iteration("int*wholeNumberDouble", "lhsInt=2", "rhsDouble=3.0", "expDouble=6.0"), + Iteration("commutativity", "lhsInt=3", "rhsDouble=2.0", "expDouble=6.0"), + Iteration("int*-double", "lhsInt=2", "rhsDouble=-3.14", "expDouble=-6.28"), + Iteration("-int*double", "lhsInt=-2", "rhsDouble=3.14", "expDouble=-6.28"), + Iteration("-int*-double", "lhsInt=-2", "rhsDouble=-3.14", "expDouble=6.28") + ) + fun testTimes_intAndDouble_returnsDouble() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal * rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity*identity", "lhsFrac=1/1", "rhsInt=1", "expFrac=1"), + Iteration("fraction*identity", "lhsFrac=2/1", "rhsInt=1", "expFrac=2"), + Iteration("fraction*int", "lhsFrac=1/3", "rhsInt=2", "expFrac=2/3"), + Iteration("wholeNumberFraction*int", "lhsFrac=3/1", "rhsInt=2", "expFrac=6"), + Iteration("commutativity", "lhsFrac=2/1", "rhsInt=3", "expFrac=6"), + Iteration("fraction*-int", "lhsFrac=1/3", "rhsInt=-2", "expFrac=-2/3"), + Iteration("-fraction*int", "lhsFrac=-1/3", "rhsInt=2", "expFrac=-2/3"), + Iteration("-fraction*-int", "lhsFrac=-1/3", "rhsInt=-2", "expFrac=2/3") + ) + fun testTimes_fractionAndInt_returnsFraction() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal * rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity*identity", "lhsFrac=1/1", "rhsFrac=1/1", "expFrac=1"), + Iteration("fraction*identity", "lhsFrac=3/2", "rhsFrac=1/1", "expFrac=1 1/2"), + Iteration("fraction*fraction", "lhsFrac=3/2", "rhsFrac=4/7", "expFrac=6/7"), + Iteration("commutativity", "lhsFrac=4/7", "rhsFrac=3/2", "expFrac=6/7"), + Iteration("fraction*-fraction", "lhsFrac=1 3/9", "rhsFrac=-8/11", "expFrac=-32/33"), + Iteration("-fraction*fraction", "lhsFrac=-1 3/9", "rhsFrac=8/11", "expFrac=-32/33"), + Iteration("-fraction*-fraction", "lhsFrac=-1 3/9", "rhsFrac=-8/11", "expFrac=32/33") + ) + fun testTimes_fractionAndFraction_returnsFraction() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal * rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity*identity", "lhsFrac=1/1", "rhsDouble=1.0", "expDouble=1.0"), + Iteration("fraction*identity", "lhsFrac=3/2", "rhsDouble=1.0", "expDouble=1.5"), + Iteration("fraction*double", "lhsFrac=3/2", "rhsDouble=3.14", "expDouble=4.71"), + Iteration("wholeNumberFraction*double", "lhsFrac=3/1", "rhsDouble=2.0", "expDouble=6.0"), + Iteration("commutativity", "lhsFrac=2/1", "rhsDouble=3.0", "expDouble=6.0"), + Iteration("fraction*-double", "lhsFrac=1 3/2", "rhsDouble=-3.14", "expDouble=-7.85"), + Iteration("-fraction*double", "lhsFrac=-1 3/2", "rhsDouble=3.14", "expDouble=-7.85"), + Iteration("-fraction*-double", "lhsFrac=-1 3/2", "rhsDouble=-3.14", "expDouble=7.85") + ) + fun testTimes_fractionAndDouble_returnsDouble() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal * rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity*identity", "lhsDouble=1.0", "rhsInt=1", "expDouble=1.0"), + Iteration("double*identity", "lhsDouble=2.0", "rhsInt=1", "expDouble=2.0"), + Iteration("double*int", "lhsDouble=3.14", "rhsInt=2", "expDouble=6.28"), + Iteration("wholeNumberDouble*int", "lhsDouble=3.0", "rhsInt=2", "expDouble=6"), + Iteration("commutativity", "lhsDouble=2.0", "rhsInt=3", "expDouble=6.0"), + Iteration("double*-int", "lhsDouble=3.14", "rhsInt=-2", "expDouble=-6.28"), + Iteration("-double*int", "lhsDouble=-3.14", "rhsInt=2", "expDouble=-6.28"), + Iteration("-double*-int", "lhsDouble=-3.14", "rhsInt=-2", "expDouble=6.28") + ) + fun testTimes_doubleAndInt_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal * rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity*identity", "lhsDouble=1.0", "rhsFrac=1/1", "expDouble=1.0"), + Iteration("double*identity", "lhsDouble=2.0", "rhsFrac=1/1", "expDouble=2.0"), + Iteration("double*fraction", "lhsDouble=3.14", "rhsFrac=3/2", "expDouble=4.71"), + Iteration("double*wholeNumberFraction", "lhsDouble=2.0", "rhsFrac=3/1", "expDouble=6.0"), + Iteration("commutativity", "lhsDouble=3.0", "rhsFrac=2/1", "expDouble=6.0"), + Iteration("double*-fraction", "lhsDouble=3.14", "rhsFrac=-1 3/2", "expDouble=-7.85"), + Iteration("-double*fraction", "lhsDouble=-3.14", "rhsFrac=1 3/2", "expDouble=-7.85"), + Iteration("-double*-fraction", "lhsDouble=-3.14", "rhsFrac=-1 3/2", "expDouble=7.85") + ) + fun testTimes_doubleAndFraction_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal * rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity*identity", "lhsDouble=1.0", "rhsDouble=1.0", "expDouble=1.0"), + Iteration("double*identity", "lhsDouble=2.0", "rhsDouble=1.0", "expDouble=2.0"), + Iteration("double*double", "lhsDouble=3.14", "rhsDouble=2.7", "expDouble=8.478"), + Iteration("commutativity", "lhsDouble=2.7", "rhsDouble=3.14", "expDouble=8.478"), + Iteration("double*-double", "lhsDouble=3.14", "rhsDouble=-2.7", "expDouble=-8.478"), + Iteration("-double*double", "lhsDouble=-3.14", "rhsDouble=2.7", "expDouble=-8.478"), + Iteration("-double*-double", "lhsDouble=-3.14", "rhsDouble=-2.7", "expDouble=8.478") + ) + fun testTimes_doubleAndDouble_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal * rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + // Division tests. + + @Test + @RunParameterized( + Iteration("identity/identity", "lhsInt=1", "rhsInt=1", "expInt=1"), + Iteration("int/identity", "lhsInt=2", "rhsInt=1", "expInt=2"), + Iteration("int/int", "lhsInt=8", "rhsInt=2", "expInt=4"), + Iteration("int/-int", "lhsInt=8", "rhsInt=-2", "expInt=-4"), + Iteration("-int/int", "lhsInt=-8", "rhsInt=2", "expInt=-4"), + Iteration("-int/-int", "lhsInt=-8", "rhsInt=-2", "expInt=4") + ) + fun testDiv_intAndInt_divides_returnsInt() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal / rhsReal + + // If the divisor divides the dividend, the result is an integer. + assertThat(result).isIntegerThat().isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("int/int", "lhsInt=7", "rhsInt=2", "expFrac=3 1/2"), + Iteration("anticommutativity", "lhsInt=2", "rhsInt=7", "expFrac=2/7"), + Iteration("int/-int", "lhsInt=7", "rhsInt=-2", "expFrac=-3 1/2"), + Iteration("-int/int", "lhsInt=-7", "rhsInt=2", "expFrac=-3 1/2"), + Iteration("-int/-int", "lhsInt=-7", "rhsInt=-2", "expFrac=3 1/2") + ) + fun testDiv_intAndInt_doesNotDivide_returnsFraction() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal / rhsReal + + // If the divisor doesn't divide the dividend, the result is a fraction. + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity/identity", "lhsInt=1", "rhsFrac=1", "expFrac=1"), + Iteration("int/identity", "lhsInt=2", "rhsFrac=1", "expFrac=2"), + Iteration("int/fraction", "lhsInt=4", "rhsFrac=1/3", "expFrac=12"), + Iteration("int/wholeNumberFraction", "lhsInt=2", "rhsFrac=3/1", "expFrac=2/3"), + Iteration("anticommutativity", "lhsInt=3", "rhsFrac=2/1", "expFrac=1 1/2"), + Iteration("int/-fraction", "lhsInt=5", "rhsFrac=-2/3", "expFrac=-7 1/2"), + Iteration("-int/fraction", "lhsInt=-5", "rhsFrac=2/3", "expFrac=-7 1/2"), + Iteration("-int/-fraction", "lhsInt=-5", "rhsFrac=-2/3", "expFrac=7 1/2") + ) + fun testDiv_intAndFraction_returnsFraction() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal / rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity/identity", "lhsInt=1", "rhsDouble=1.0", "expDouble=1.0"), + Iteration("int/identity", "lhsInt=2", "rhsDouble=1.0", "expDouble=2.0"), + Iteration("int/double", "lhsInt=2", "rhsDouble=3.14", "expDouble=0.636942675"), + Iteration("int/wholeNumberDouble", "lhsInt=2", "rhsDouble=3.0", "expDouble=0.666666667"), + Iteration("anticommutativity", "lhsInt=3", "rhsDouble=2.0", "expDouble=1.5"), + Iteration("int/-double", "lhsInt=2", "rhsDouble=-3.14", "expDouble=-0.636942675"), + Iteration("-int/double", "lhsInt=-2", "rhsDouble=3.14", "expDouble=-0.636942675"), + Iteration("-int/-double", "lhsInt=-2", "rhsDouble=-3.14", "expDouble=0.636942675") + ) + fun testDiv_intAndDouble_returnsDouble() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal / rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity/identity", "lhsFrac=1/1", "rhsInt=1", "expFrac=1"), + Iteration("fraction/identity", "lhsFrac=2/1", "rhsInt=1", "expFrac=2"), + Iteration("fraction/int", "lhsFrac=1/3", "rhsInt=2", "expFrac=1/6"), + Iteration("wholeNumberFraction/int", "lhsFrac=3/1", "rhsInt=2", "expFrac=1 1/2"), + Iteration("anticommutativity", "lhsFrac=2/1", "rhsInt=3", "expFrac=2/3"), + Iteration("fraction/-int", "lhsFrac=-1 1/3", "rhsInt=2", "expFrac=-2/3"), + Iteration("-fraction/int", "lhsFrac=1 1/3", "rhsInt=-2", "expFrac=-2/3"), + Iteration("-fraction/-int", "lhsFrac=-1 1/3", "rhsInt=-2", "expFrac=2/3") + ) + fun testDiv_fractionAndInt_returnsFraction() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal / rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity/identity", "lhsFrac=1/1", "rhsFrac=1/1", "expFrac=1"), + Iteration("fraction/identity", "lhsFrac=3/2", "rhsFrac=1/1", "expFrac=1 1/2"), + Iteration("fraction/fraction", "lhsFrac=3/2", "rhsFrac=4/7", "expFrac=2 5/8"), + Iteration("anticommutativity", "lhsFrac=4/7", "rhsFrac=3/2", "expFrac=8/21"), + Iteration("fraction/-fraction", "lhsFrac=1 3/9", "rhsFrac=-8/11", "expFrac=-1 5/6"), + Iteration("-fraction/fraction", "lhsFrac=-1 3/9", "rhsFrac=8/11", "expFrac=-1 5/6"), + Iteration("-fraction/-fraction", "lhsFrac=-1 3/9", "rhsFrac=-8/11", "expFrac=1 5/6") + ) + fun testDiv_fractionAndFraction_returnsFraction() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal / rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity/identity", "lhsFrac=1/1", "rhsDouble=1.0", "expDouble=1.0"), + Iteration("fraction/identity", "lhsFrac=3/2", "rhsDouble=1.0", "expDouble=1.5"), + Iteration("fraction/double", "lhsFrac=3/2", "rhsDouble=3.14", "expDouble=0.477707006"), + Iteration("wholeNumberFraction/double", "lhsFrac=3/1", "rhsDouble=2.0", "expDouble=1.5"), + Iteration("anticommutativity", "lhsFrac=2/1", "rhsDouble=3.0", "expDouble=0.666666667"), + Iteration("fraction/-double", "lhsFrac=1 3/2", "rhsDouble=-3.14", "expDouble=-0.796178344"), + Iteration("-fraction/double", "lhsFrac=-1 3/2", "rhsDouble=3.14", "expDouble=-0.796178344"), + Iteration("-fraction/-double", "lhsFrac=-1 3/2", "rhsDouble=-3.14", "expDouble=0.796178344") + ) + fun testDiv_fractionAndDouble_returnsDouble() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal / rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity/identity", "lhsDouble=1.0", "rhsInt=1", "expDouble=1.0"), + Iteration("double/identity", "lhsDouble=2.0", "rhsInt=1", "expDouble=2.0"), + Iteration("double/int", "lhsDouble=3.14", "rhsInt=2", "expDouble=1.57"), + Iteration("wholeNumberDouble/int", "lhsDouble=3.0", "rhsInt=2", "expDouble=1.5"), + Iteration("anticommutativity", "lhsDouble=2.0", "rhsInt=3", "expDouble=0.666666667"), + Iteration("double/-int", "lhsDouble=3.14", "rhsInt=-2", "expDouble=-1.57"), + Iteration("-double/int", "lhsDouble=-3.14", "rhsInt=2", "expDouble=-1.57"), + Iteration("-double/-int", "lhsDouble=-3.14", "rhsInt=-2", "expDouble=1.57") + ) + fun testDiv_doubleAndInt_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal / rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity/identity", "lhsDouble=1.0", "rhsFrac=1/1", "expDouble=1.0"), + Iteration("double/identity", "lhsDouble=2.0", "rhsFrac=1/1", "expDouble=2.0"), + Iteration("double/fraction", "lhsDouble=3.14", "rhsFrac=3/2", "expDouble=2.093333333"), + Iteration("double/wholeNumberFraction", "lhsDouble=2.0", "rhsFrac=3/1", "expDouble=0.66666667"), + Iteration("anticommutativity", "lhsDouble=3.0", "rhsFrac=2/1", "expDouble=1.5"), + Iteration("double/-fraction", "lhsDouble=3.14", "rhsFrac=-1 3/2", "expDouble=-1.256"), + Iteration("-double/fraction", "lhsDouble=-3.14", "rhsFrac=1 3/2", "expDouble=-1.256"), + Iteration("-double/-fraction", "lhsDouble=-3.14", "rhsFrac=-1 3/2", "expDouble=1.256") + ) + fun testDiv_doubleAndFraction_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal / rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity/identity", "lhsDouble=1.0", "rhsDouble=1.0", "expDouble=1.0"), + Iteration("double/identity", "lhsDouble=2.0", "rhsDouble=1.0", "expDouble=2.0"), + Iteration("double/double", "lhsDouble=3.14", "rhsDouble=2.7", "expDouble=1.162962963"), + Iteration("anticommutativity", "lhsDouble=2.7", "rhsDouble=3.14", "expDouble=0.859872611"), + Iteration("double/-double", "lhsDouble=3.14", "rhsDouble=-2.7", "expDouble=-1.162962963"), + Iteration("-double/double", "lhsDouble=-3.14", "rhsDouble=2.7", "expDouble=-1.162962963"), + Iteration("-double/-double", "lhsDouble=-3.14", "rhsDouble=-2.7", "expDouble=1.162962963") + ) + fun testDiv_doubleAndDouble_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal / rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + fun testDiv_intDividedByZeroInt_throwsException() { + val lhsReal = createIntegerReal(2) + val rhsReal = createIntegerReal(0) + + assertThrows(ArithmeticException::class) { lhsReal / rhsReal } + } + + @Test + fun testDiv_intDividedByZeroFraction_throwsException() { + val lhsReal = createIntegerReal(2) + val rhsReal = createRationalReal(ZERO_FRACTION) + + assertThrows(ArithmeticException::class) { lhsReal / rhsReal } + } + + @Test + fun testDiv_intDividedByZeroDouble_returnsInfinityDouble() { + val lhsReal = createIntegerReal(2) + val rhsReal = createIrrationalReal(0.0) + + val result = lhsReal / rhsReal + + assertThat(result).isEqualTo(createIrrationalReal(Double.POSITIVE_INFINITY)) + } + + @Test + fun testDiv_fractionDividedByZeroInt_throwsException() { + val lhsReal = ONE_AND_ONE_HALF_REAL + val rhsReal = createIntegerReal(0) + + assertThrows(ArithmeticException::class) { lhsReal / rhsReal } + } + + @Test + fun testDiv_fractionDividedByZeroFraction_throwsException() { + val lhsReal = ONE_AND_ONE_HALF_REAL + val rhsReal = createRationalReal(ZERO_FRACTION) + + assertThrows(ArithmeticException::class) { lhsReal / rhsReal } + } + + @Test + fun testDiv_fractionDividedByZeroDouble_returnsInfinityDouble() { + val lhsReal = ONE_AND_ONE_HALF_REAL + val rhsReal = createIrrationalReal(0.0) + + val result = lhsReal / rhsReal + + assertThat(result).isEqualTo(createIrrationalReal(Double.POSITIVE_INFINITY)) + } + + @Test + fun testDiv_doubleDividedByZeroInt_returnsInfinityDouble() { + val lhsReal = createIrrationalReal(3.14) + val rhsReal = createIntegerReal(0) + + val result = lhsReal / rhsReal + + assertThat(result).isEqualTo(createIrrationalReal(Double.POSITIVE_INFINITY)) + } + + @Test + fun testDiv_doubleDividedByZeroFraction_returnsInfinityDouble() { + val lhsReal = createIrrationalReal(3.14) + val rhsReal = createRationalReal(ZERO_FRACTION) + + val result = lhsReal / rhsReal + + assertThat(result).isEqualTo(createIrrationalReal(Double.POSITIVE_INFINITY)) + } + + @Test + fun testDiv_doubleDividedByZeroDouble_returnsInfinityDouble() { + val lhsReal = createIrrationalReal(3.14) + val rhsReal = createIrrationalReal(0.0) + + val result = lhsReal / rhsReal + + assertThat(result).isEqualTo(createIrrationalReal(Double.POSITIVE_INFINITY)) + } + + // Exponentiation tests. + + @Test + @RunParameterized( + Iteration("0^0", "lhsInt=0", "rhsInt=0", "expInt=1"), + Iteration("identity^0", "lhsInt=1", "rhsInt=0", "expInt=1"), + Iteration("identity^identity", "lhsInt=1", "rhsInt=1", "expInt=1"), + Iteration("int^0", "lhsInt=2", "rhsInt=0", "expInt=1"), + Iteration("int^identity", "lhsInt=2", "rhsInt=1", "expInt=2"), + Iteration("int^int", "lhsInt=2", "rhsInt=3", "expInt=8"), + Iteration("noncommutativity", "lhsInt=3", "rhsInt=2", "expInt=9"), + Iteration("-int^even int", "lhsInt=-2", "rhsInt=4", "expInt=16"), + Iteration("-int^odd int", "lhsInt=-2", "rhsInt=3", "expInt=-8") + ) + fun testPow_intAndInt_positivePower_returnsInt() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal pow rhsReal + + // Integer raised to positive (or zero) integers always results in another integer. + assertThat(result).isIntegerThat().isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("int^-int", "lhsInt=2", "rhsInt=-3", "expFrac=1/8"), + Iteration("-int^-even int", "lhsInt=-2", "rhsInt=-4", "expFrac=1/16"), + Iteration("-int^-odd int", "lhsInt=-2", "rhsInt=-3", "expFrac=-1/8") + ) + fun testPow_intAndInt_negativePower_returnsFraction() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal pow rhsReal + + // Integers raised to a negative integer yields a fraction since x^-y=1/(x^y). + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("0^0", "lhsInt=0", "rhsFrac=0/1", "expFrac=1"), + Iteration("identity^0", "lhsInt=1", "rhsFrac=0/1", "expFrac=1"), + Iteration("identity^identity", "lhsInt=1", "rhsFrac=1", "expFrac=1"), + Iteration("int^0", "lhsInt=2", "rhsFrac=0/1", "expFrac=1"), + Iteration("int^identity", "lhsInt=2", "rhsFrac=1", "expFrac=2"), + Iteration("int^fraction", "lhsInt=16", "rhsFrac=3/2", "expFrac=64"), + Iteration("int^wholeNumberFraction", "lhsInt=2", "rhsFrac=3/1", "expFrac=8"), + Iteration("noncommutativity", "lhsInt=3", "rhsFrac=2/1", "expFrac=9"), + Iteration("int^odd fraction", "lhsInt=8", "rhsFrac=5/3", "expFrac=32"), + Iteration("int^-fraction", "lhsInt=8", "rhsFrac=-4/2", "expFrac=1/64"), + Iteration("-int^odd fraction", "lhsInt=-8", "rhsFrac=5/3", "expFrac=-32"), + Iteration("-int^-fraction", "lhsInt=-4", "rhsFrac=-4/2", "expFrac=1/16"), + Iteration("-int^-odd fraction", "lhsInt=-8", "rhsFrac=-5/3", "expFrac=-1/32") + ) + fun testPow_intAndFraction_denominatorCanRootInt_returnsFraction() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal pow rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("int^fraction", "lhsInt=3", "rhsFrac=2/3", "expDouble=2.080083823"), + Iteration("-int^fraction", "lhsInt=-4", "rhsFrac=2/3", "expDouble=2.5198421"), + Iteration("int^-fraction", "lhsInt=2", "rhsFrac=-2/3", "expDouble=0.629960525"), + Iteration("-int^-fraction", "lhsInt=-4", "rhsFrac=-2/3", "expDouble=0.396850263") + ) + fun testPow_intAndFraction_denominatorCannotRootInt_returnsDouble() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal pow rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("0^0", "lhsInt=0", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("identity^0", "lhsInt=1", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("identity^identity", "lhsInt=1", "rhsDouble=1.0", "expDouble=1.0"), + Iteration("int^0", "lhsInt=2", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("int^identity", "lhsInt=2", "rhsDouble=1.0", "expDouble=2.0"), + Iteration("int^double", "lhsInt=2", "rhsDouble=3.14", "expDouble=8.815240927"), + Iteration("int^wholeNumberDouble", "lhsInt=2", "rhsDouble=3.0", "expDouble=8.0"), + Iteration("noncommutativity", "lhsInt=3", "rhsDouble=2.0", "expDouble=9.0"), + Iteration("int^-double", "lhsInt=2", "rhsDouble=-3.14", "expDouble=0.113439894") + ) + fun testPow_intAndDouble_returnsDouble() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal pow rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("0^0", "lhsFrac=0", "rhsInt=0", "expFrac=1"), + Iteration("identity^0", "lhsFrac=1", "rhsInt=0", "expFrac=1"), + Iteration("identity^identity", "lhsFrac=1", "rhsInt=1", "expFrac=1"), + Iteration("fraction^0", "lhsFrac=1/3", "rhsInt=0", "expFrac=1"), + Iteration("fraction^identity", "lhsFrac=1/3", "rhsInt=1", "expFrac=1/3"), + Iteration("fraction^int", "lhsFrac=2/3", "rhsInt=3", "expFrac=8/27"), + Iteration("wholeNumberFraction^int", "lhsFrac=3", "rhsInt=2", "expFrac=9"), + Iteration("noncommutativity", "lhsFrac=2", "rhsInt=3", "expFrac=8"), + Iteration("fraction^-int", "lhsFrac=4/3", "rhsInt=-2", "expFrac=9/16"), + Iteration("-fraction^int", "lhsFrac=-4/3", "rhsInt=2", "expFrac=1 7/9"), + Iteration("-fraction^-int", "lhsFrac=-4/3", "rhsInt=-2", "expFrac=9/16") + ) + fun testPow_fractionAndInt_returnsFraction() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal pow rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("0^0", "lhsFrac=0", "rhsFrac=0", "expFrac=1"), + Iteration("identity^0", "lhsFrac=1", "rhsFrac=0", "expFrac=1"), + Iteration("identity^identity", "lhsFrac=1", "rhsFrac=1", "expFrac=1"), + Iteration("fraction^0", "lhsFrac=3/2", "rhsFrac=0", "expFrac=1"), + Iteration("fraction^identity", "lhsFrac=3/2", "rhsFrac=1", "expFrac=1 1/2"), + Iteration("fraction^fraction", "lhsFrac=32/243", "rhsFrac=3/5", "expFrac=8/27"), + Iteration("fraction^wholeNumberFraction", "lhsFrac=3", "rhsFrac=2", "expFrac=9"), + Iteration("noncommutativity", "lhsFrac=2", "rhsFrac=3", "expFrac=8"), + Iteration("fraction^-fraction", "lhsFrac=32/243", "rhsFrac=-3/5", "expFrac=3 3/8") + ) + fun testPow_fractionAndFraction_denominatorCanRootFraction_returnsFraction() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal pow rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("fraction^fraction", "lhsFrac=3/2", "rhsFrac=2/3", "expDouble=1.310370697"), + Iteration("noncommutativity", "lhsFrac=2/3", "rhsFrac=3/2", "expDouble=0.544331054"), + Iteration("fraction^-fraction", "lhsFrac=3/2", "rhsFrac=-2/3", "expDouble=0.763142828") + ) + fun testPow_fractionAndFraction_denominatorCannotRootFraction_returnsDouble() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal pow rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("0^0", "lhsFrac=0", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("identity^0", "lhsFrac=1", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("identity^identity", "lhsFrac=1", "rhsDouble=1.0", "expDouble=1.0"), + Iteration("fraction^0", "lhsFrac=3/2", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("fraction^identity", "lhsFrac=3/2", "rhsDouble=1.0", "expDouble=1.5"), + Iteration("fraction^double", "lhsFrac=3/2", "rhsDouble=3.14", "expDouble=3.572124224"), + Iteration("wholeNumberFraction^double", "lhsFrac=3", "rhsDouble=2.0", "expDouble=9.0"), + Iteration("noncommutativity", "lhsFrac=2", "rhsDouble=3.0", "expDouble=8.0"), + Iteration("fraction^-double", "lhsFrac=1 3/2", "rhsDouble=-3.14", "expDouble=0.056294812") + ) + fun testPow_fractionAndDouble_returnsDouble() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal pow rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("0^0", "lhsDouble=0.0", "rhsInt=0", "expDouble=1.0"), + Iteration("identity^0", "lhsDouble=1.0", "rhsInt=0", "expDouble=1.0"), + Iteration("identity^identity", "lhsDouble=1.0", "rhsInt=1", "expDouble=1.0"), + Iteration("double^0", "lhsDouble=3.14", "rhsInt=0", "expDouble=1.0"), + Iteration("double^identity", "lhsDouble=3.14", "rhsInt=1", "expDouble=3.14"), + Iteration("double^int", "lhsDouble=3.14", "rhsInt=2", "expDouble=9.8596"), + Iteration("wholeNumberDouble^int", "lhsDouble=3.0", "rhsInt=2", "expDouble=9.0"), + Iteration("noncommutativity", "lhsDouble=2.0", "rhsInt=3", "expDouble=8.0"), + Iteration("double^-int", "lhsDouble=3.14", "rhsInt=-3", "expDouble=0.032300635"), + Iteration("-double^int", "lhsDouble=-3.14", "rhsInt=3", "expDouble=-30.959144"), + Iteration("-double^-int", "lhsDouble=-3.14", "rhsInt=-3", "expDouble=-0.032300635") + ) + fun testPow_doubleAndInt_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal pow rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("0^0", "lhsDouble=0.0", "rhsFrac=0/1", "expDouble=1.0"), + Iteration("identity^0", "lhsDouble=1.0", "rhsFrac=0/1", "expDouble=1.0"), + Iteration("identity^identity", "lhsDouble=1.0", "rhsFrac=1", "expDouble=1.0"), + Iteration("double^0", "lhsDouble=3.14", "rhsFrac=0/1", "expDouble=1.0"), + Iteration("double^identity", "lhsDouble=3.14", "rhsFrac=1", "expDouble=3.14"), + Iteration("double^fraction", "lhsDouble=3.14", "rhsFrac=3/2", "expDouble=5.564094176"), + Iteration("double^wholeNumberFraction", "lhsDouble=2.0", "rhsFrac=3/1", "expDouble=8.0"), + Iteration("noncommutativity", "lhsDouble=3.0", "rhsFrac=2/1", "expDouble=9.0"), + Iteration("double^-fraction", "lhsDouble=3.14", "rhsFrac=-3/2", "expDouble=0.179723773") + ) + fun testPow_doubleAndFraction_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal pow rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("0^0", "lhsDouble=0.0", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("identity^0", "lhsDouble=1.0", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("identity^identity", "lhsDouble=1.0", "rhsDouble=1.0", "expDouble=1.0"), + Iteration("double^0", "lhsDouble=3.14", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("double^identity", "lhsDouble=3.14", "rhsDouble=1.0", "expDouble=3.14"), + Iteration("double^double", "lhsDouble=3.14", "rhsDouble=2.7", "expDouble=21.963929943"), + Iteration("noncommutativity", "lhsDouble=2.7", "rhsDouble=3.14", "expDouble=22.619459311"), + Iteration("double^-double", "lhsDouble=3.14", "rhsDouble=-2.7", "expDouble=0.045529193") + ) + fun testPow_doubleAndDouble_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal pow rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + fun testPow_negativeIntToOneHalfFraction_throwsException() { + val lhsReal = createIntegerReal(-3) + val rhsReal = createRationalReal(ONE_HALF_FRACTION) + + val exception = assertThrows(IllegalStateException::class) { lhsReal pow rhsReal } + + assertThat(exception).hasMessageThat().contains("Radicand results in imaginary number") + } + + @Test + fun testPow_negativeIntToNonzeroDouble_returnsNotANumber() { + val lhsReal = createIntegerReal(-3) + val rhsReal = createIrrationalReal(3.14) + + val result = lhsReal pow rhsReal + + assertThat(result).isIrrationalThat().isNaN() + } + + @Test + fun testPow_negativeFractionToOneHalfFraction_throwsException() { + val lhsReal = NEGATIVE_ONE_AND_ONE_HALF_REAL + val rhsReal = createRationalReal(ONE_HALF_FRACTION) + + val exception = assertThrows(IllegalStateException::class) { lhsReal pow rhsReal } + + assertThat(exception).hasMessageThat().contains("Radicand results in imaginary number") + } + + @Test + fun testPow_negativeFractionToNegativeFractionWithOddNumerator_throwsException() { + val lhsReal = createRationalReal((-4).toWholeNumberFraction()) + val rhsReal = createRationalReal(-ONE_AND_ONE_HALF_FRACTION) + + val exception = assertThrows(IllegalStateException::class) { lhsReal pow rhsReal } + + assertThat(exception).hasMessageThat().contains("Radicand results in imaginary number") + } + + @Test + fun testPow_negativeFractionToNonzeroDouble_returnsNotANumber() { + val lhsReal = NEGATIVE_ONE_AND_ONE_HALF_REAL + val rhsReal = createIrrationalReal(3.14) + + val result = lhsReal pow rhsReal + + assertThat(result).isIrrationalThat().isNaN() + } + + @Test + fun testPow_negativeDoubleToOneHalfFraction_returnsNotANumber() { + val lhsReal = createIrrationalReal(-2.7) + val rhsReal = createRationalReal(ONE_HALF_FRACTION) + + val result = lhsReal pow rhsReal + + assertThat(result).isIrrationalThat().isNaN() + } + + @Test + fun testPow_negativeDoubleToNonzeroDouble_returnsNotANumber() { + val lhsReal = createIrrationalReal(-2.7) + val rhsReal = createIrrationalReal(3.14) + + val result = lhsReal pow rhsReal + + assertThat(result).isIrrationalThat().isNaN() + } + + /* End operator tests. */ + + @Test + fun testSqrt_defaultReal_throwsException() { + val real = Real.getDefaultInstance() + + val exception = assertThrows(IllegalStateException::class) { sqrt(real) } + + assertThat(exception).hasMessageThat().contains("Invalid real") + } + + @Test + fun testSqrt_negativeInteger_throwsException() { + val real = createIntegerReal(-2) + + val exception = assertThrows(IllegalStateException::class) { sqrt(real) } + + assertThat(exception).hasMessageThat().contains("Radicand results in imaginary number") + } + + @Test + fun testSqrt_zeroInteger_returnsZeroInteger() { + val real = createIntegerReal(0) + + val result = sqrt(real) + + assertThat(result).isIntegerThat().isEqualTo(0) + } + + @Test + fun testSqrt_fourInteger_returnsTwoInteger() { + val real = createIntegerReal(4) + + val result = sqrt(real) + + assertThat(result).isIntegerThat().isEqualTo(2) + } + + @Test + fun testSqrt_fourTwo_returnsSqrtTwoDouble() { + val real = createIntegerReal(2) + + val result = sqrt(real) + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(1.414213562) + } + + @Test + fun testSqrt_negativeFraction_throwsException() { + val real = createRationalReal((-2).toWholeNumberFraction()) + + val exception = assertThrows(IllegalStateException::class) { sqrt(real) } + + assertThat(exception).hasMessageThat().contains("Radicand results in imaginary number") + } + + @Test + fun testSqrt_zeroFraction_returnZeroFraction() { + val real = createRationalReal(ZERO_FRACTION) + + val result = sqrt(real) + + assertThat(result).isRationalThat().isEqualTo(ZERO_FRACTION) + } + + @Test + fun testSqrt_fourFraction_returnsTwoFraction() { + val real = createRationalReal(4.toWholeNumberFraction()) + + val result = sqrt(real) + + assertThat(result).isRationalThat().isEqualTo(2.toWholeNumberFraction()) + } + + @Test + fun testSqrt_oneFourthFraction_returnsOneHalfFraction() { + val real = createRationalReal(ONE_FOURTH_FRACTION) + + val result = sqrt(real) + + assertThat(result).isRationalThat().isEqualTo(ONE_HALF_FRACTION) + } + + @Test + fun testSqrt_sixteenthNinthsFraction_returnsOneAndOneThirdFraction() { + val real = createRationalReal(createFraction(numerator = 16, denominator = 9)) + + val result = sqrt(real) + + // Verify that both the numerator and denominator are properly rooted, and that a proper + // fraction is returned. + assertThat(result).isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(1) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(3) + } + } + + @Test + fun testSqrt_twoThirdsFraction_returnsComputedDouble() { + val real = createRationalReal(createFraction(numerator = 2, denominator = 3)) + + val result = sqrt(real) + + // sqrt(2/3) can't be computed perfectly, so a double must be computed, instead. + assertThat(result).isIrrationalThat().isWithin(1e-5).of(0.816496581) + } + + @Test + fun testSqrt_negativeDouble_returnsNotANumber() { + val real = createIrrationalReal(-2.7) + + val result = sqrt(real) + + assertThat(result).isIrrationalThat().isNaN() + } + + @Test + fun testSqrt_zeroDouble_returnsZeroDouble() { + val real = createIrrationalReal(0.0) + + val result = sqrt(real) + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(0.0) + } + + @Test + fun testSqrt_fourDouble_returnsTwoDouble() { + val real = createIrrationalReal(4.0) + + val result = sqrt(real) + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(2.0) + } + + @Test + fun testSqrt_twoDouble_returnsRootTwoDouble() { + val real = createIrrationalReal(2.0) + + val result = sqrt(real) + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(1.414213562) + } + + @Test + fun testSqrt_nonWholeDouble_returnsCorrectSquareRootDouble() { + val real = createIrrationalReal(3.14) + + val result = sqrt(real) + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(1.772004515) + } } private fun createIntegerReal(value: Int) = Real.newBuilder().apply { integer = value }.build() +private fun createRationalReal(rawFractionExpression: String) = + createRationalReal(parseFraction(rawFractionExpression)) + private fun createRationalReal(value: Fraction) = Real.newBuilder().apply { rational = value }.build() @@ -412,3 +1788,8 @@ private fun createRationalReal(value: Fraction) = Real.newBuilder().apply { private fun createIrrationalReal(value: Double) = Real.newBuilder().apply { irrational = value }.build() + +private fun createFraction(numerator: Int, denominator: Int) = Fraction.newBuilder().apply { + this.numerator = numerator + this.denominator = denominator +}.build() From 19a64251d92dcf1f268fd61d6635e2736228dda4 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 26 Jan 2022 15:47:28 -0800 Subject: [PATCH 061/134] Split StringToFractionParser. This is a temporary change that will be finished upstream (since there's an earlier PR that's a better fit for this change). --- .../app/parser/StringToFractionParser.kt | 65 ++----------------- domain/BUILD.bazel | 1 + .../org/oppia/android/domain/util/BUILD.bazel | 1 - .../oppia/android/util/extensions/BUILD.bazel | 8 +++ .../util/extensions}/StringExtensions.kt | 0 5 files changed, 16 insertions(+), 59 deletions(-) rename {domain/src/main/java/org/oppia/android/domain/util => utility/src/main/java/org/oppia/android/util/extensions}/StringExtensions.kt (100%) diff --git a/app/src/main/java/org/oppia/android/app/parser/StringToFractionParser.kt b/app/src/main/java/org/oppia/android/app/parser/StringToFractionParser.kt index 62fc2e9a282..0b806b263d7 100644 --- a/app/src/main/java/org/oppia/android/app/parser/StringToFractionParser.kt +++ b/app/src/main/java/org/oppia/android/app/parser/StringToFractionParser.kt @@ -5,30 +5,25 @@ 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.math.FractionParser /** This class contains method that helps to parse string to fraction. */ class StringToFractionParser { - private val wholeNumberOnlyRegex = - """^-? ?(\d+)$""".toRegex() - private val fractionOnlyRegex = - """^-? ?(\d+) ?/ ?(\d+)$""".toRegex() - private val mixedNumberRegex = - """^-? ?(\d+) (\d+) ?/ ?(\d+)$""".toRegex() - private val invalidCharsRegex = - """^[\d\s/-]+$""".toRegex() + private val invalidCharsRegex = """^[\d\s/-]+$""".toRegex() private val invalidCharsLengthRegex = "\\d{8,}".toRegex() /** * Returns a [FractionParsingError] for the specified text input if it's an invalid fraction, or * [FractionParsingError.VALID] if no issues are found. Note that a valid fraction returned by - * this method is guaranteed to be parsed correctly by [parseRegularFraction]. + * this method is guaranteed to be parsed correctly by [parseFraction]. * * This method should only be used when a user tries submitting an answer. Real-time error * detection should be done using [getRealTimeAnswerError], instead. */ fun getSubmitTimeError(text: String): FractionParsingError { - if (invalidCharsLengthRegex.find(text) != null) + if (invalidCharsLengthRegex.find(text) != null) { return FractionParsingError.NUMBER_TOO_LONG + } val fraction = parseFraction(text) return when { fraction == null -> FractionParsingError.INVALID_FORMAT @@ -57,56 +52,10 @@ class StringToFractionParser { } /** Returns a [Fraction] parse from the specified raw text string. */ - fun parseFraction(text: String): Fraction? { - // Normalize whitespace to ensure that answer follows a simpler subset of possible patterns. - val inputText: String = text.normalizeWhitespace() - return parseMixedNumber(inputText) - ?: parseRegularFraction(inputText) - ?: parseWholeNumber(inputText) - } + fun parseFraction(text: String): Fraction? = FractionParser.tryParseFraction(text) /** Returns a [Fraction] parse from the specified raw text string. */ - fun parseFractionFromString(text: String): Fraction { - return parseFraction(text) - ?: throw IllegalArgumentException("Incorrectly formatted fraction: $text") - } - - private fun parseMixedNumber(inputText: String): Fraction? { - val mixedNumberMatch = mixedNumberRegex.matchEntire(inputText) ?: return null - val (_, wholeNumberText, numeratorText, denominatorText) = - mixedNumberMatch.groupValues - return Fraction.newBuilder() - .setIsNegative(isInputNegative(inputText)) - .setWholeNumber(wholeNumberText.toInt()) - .setNumerator(numeratorText.toInt()) - .setDenominator(denominatorText.toInt()) - .build() - } - - private fun parseRegularFraction(inputText: String): Fraction? { - val fractionOnlyMatch = fractionOnlyRegex.matchEntire(inputText) ?: return null - val (_, numeratorText, denominatorText) = fractionOnlyMatch.groupValues - // Fraction-only numbers imply no whole number. - return Fraction.newBuilder() - .setIsNegative(isInputNegative(inputText)) - .setNumerator(numeratorText.toInt()) - .setDenominator(denominatorText.toInt()) - .build() - } - - private fun parseWholeNumber(inputText: String): Fraction? { - val wholeNumberMatch = wholeNumberOnlyRegex.matchEntire(inputText) ?: return null - val (_, wholeNumberText) = wholeNumberMatch.groupValues - // Whole number fractions imply '0/1' fractional parts. - return Fraction.newBuilder() - .setIsNegative(isInputNegative(inputText)) - .setWholeNumber(wholeNumberText.toInt()) - .setNumerator(0) - .setDenominator(1) - .build() - } - - private fun isInputNegative(inputText: String): Boolean = inputText.startsWith("-") + fun parseFractionFromString(text: String): Fraction = FractionParser.parseFraction(text) /** Enum to store the errors of [FractionInputInteractionView]. */ enum class FractionParsingError(@StringRes private var error: Int?) { diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index 6d15ff50d73..49bdc4dbf0c 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -122,6 +122,7 @@ 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", 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 7c1dc1e6ce2..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 @@ -23,7 +23,6 @@ kt_android_library( srcs = [ "InteractionObjectExtensions.kt", "JsonExtensions.kt", - "StringExtensions.kt", "WorkDataExtensions.kt", ], visibility = ["//domain:__subpackages__"], 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 100% 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 From 7a364c39eb9c996766e0e201d89b6ae66de05fc3 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 26 Jan 2022 16:16:12 -0800 Subject: [PATCH 062/134] Address reviewer comments + other stuff. This also fixes a typo and incorrectly ordered exemptions list I noticed during development of downstream PRs. --- model/src/main/proto/math.proto | 6 +++--- scripts/assets/test_file_exemptions.textproto | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index a8af5ba5bec..d648acad614 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -36,7 +36,7 @@ message Real { // decimal values need to be treated as irrational and non-factorable. double irrational = 2; - // Indicates that thi sreal value is an integer (as a special case of rational values since + // Indicates that this real value is an integer (as a special case of rational values since // integers are easier to work with than fraction objects). Note that this isn't the only case // where the real value can be an integer. It can also be an integer double value, or a fraction // with only a whole number component. @@ -60,12 +60,12 @@ message RatioExpression { // Values of this proto can be analyzed using MathExpressionSubject. message MathExpression { // The index within the input text stream at which point the expression starts (it's an inclusive - // index). If both this and the end index are zero then no parsing information is included for + // index). If both this and the end index are the same then no parsing information is included for // this specific expression. uint32 parse_start_index = 1; // The index within the input text stream at which point the expression ends, exclusively. If both - // this and the start index are zero then no parsing information is included for this specific + // this and the start index are the same then no parsing information is included for this specific // expression. uint32 parse_end_index = 2; diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index 6d52e0b504a..d852a048e85 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -647,9 +647,9 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/Im exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/KonfettiViewMatcher.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/DefineAppLanguageLocaleContext.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt" -exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt" -exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/mockito/MockitoKotlinHelper.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/network/ApiMockLoader.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/network/MockClassroomService.kt" From cfe6cab959f04da6c8f23be9cfb95e34068d6dcb Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 26 Jan 2022 18:11:14 -0800 Subject: [PATCH 063/134] Move StringExtensions & fraction parsing. This splits fraction parsing between UI & utility components. --- app/BUILD.bazel | 3 +- .../FractionInputInteractionView.kt | 2 + .../app/parser/FractionParsingUiError.kt | 36 ++ .../app/parser/StringToNumberParser.kt | 2 +- .../android/app/parser/StringToRatioParser.kt | 4 +- .../FractionInteractionViewModel.kt | 23 +- .../InputInteractionViewTestActivityTest.kt | 2 + .../app/parser/FractionParsingUiErrorTest.kt | 271 ++++++++++ .../app/parser/StringToFractionParserTest.kt | 509 ------------------ domain/BUILD.bazel | 1 + ...TextInputContainsRuleClassifierProvider.kt | 2 +- .../TextInputEqualsRuleClassifierProvider.kt | 2 +- ...tInputFuzzyEqualsRuleClassifierProvider.kt | 2 +- ...xtInputStartsWithRuleClassifierProvider.kt | 2 +- .../org/oppia/android/domain/util/BUILD.bazel | 1 - .../domain/util/StringExtensionsTest.kt | 2 + .../oppia/android/util/extensions/BUILD.bazel | 8 + .../util/extensions}/StringExtensions.kt | 2 +- .../org/oppia/android/util/math/BUILD.bazel | 12 + .../oppia/android/util/math/FractionParser.kt | 43 +- .../org/oppia/android/util/math/BUILD.bazel | 18 + .../android/util/math/FractionParserTest.kt | 280 ++++++++++ 22 files changed, 681 insertions(+), 546 deletions(-) create mode 100644 app/src/main/java/org/oppia/android/app/parser/FractionParsingUiError.kt create mode 100644 app/src/test/java/org/oppia/android/app/parser/FractionParsingUiErrorTest.kt delete mode 100644 app/src/test/java/org/oppia/android/app/parser/StringToFractionParserTest.kt rename {domain/src/main/java/org/oppia/android/domain/util => utility/src/main/java/org/oppia/android/util/extensions}/StringExtensions.kt (92%) rename app/src/main/java/org/oppia/android/app/parser/StringToFractionParser.kt => utility/src/main/java/org/oppia/android/util/math/FractionParser.kt (80%) create mode 100644 utility/src/test/java/org/oppia/android/util/math/FractionParserTest.kt diff --git a/app/BUILD.bazel b/app/BUILD.bazel index db1015977b4..f170cc195ae 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..fb1991cca59 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/parser/FractionParsingUiError.kt @@ -0,0 +1,36 @@ +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?) { + 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); + + /** + * 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/sharedTest/java/org/oppia/android/app/testing/InputInteractionViewTestActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/InputInteractionViewTestActivityTest.kt index d9100849877..a154034f6dc 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 @@ -122,6 +122,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..0342d7e3539 --- /dev/null +++ b/app/src/test/java/org/oppia/android/app/parser/FractionParsingUiErrorTest.kt @@ -0,0 +1,271 @@ +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 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 1d71c8c8afd..00000000000 --- a/app/src/test/java/org/oppia/android/app/parser/StringToFractionParserTest.kt +++ /dev/null @@ -1,509 +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 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 6d15ff50d73..49bdc4dbf0c 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -122,6 +122,7 @@ 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", 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 7c1dc1e6ce2..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 @@ -23,7 +23,6 @@ kt_android_library( srcs = [ "InteractionObjectExtensions.kt", "JsonExtensions.kt", - "StringExtensions.kt", "WorkDataExtensions.kt", ], visibility = ["//domain:__subpackages__"], 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/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 index 4ecd3e58e47..cae9779bc08 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -18,3 +18,15 @@ kt_android_library( "//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/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 80% 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 62fc2e9a282..659153b98f9 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 = @@ -27,8 +24,9 @@ class StringToFractionParser { * detection should be done using [getRealTimeAnswerError], instead. */ fun getSubmitTimeError(text: String): FractionParsingError { - if (invalidCharsLengthRegex.find(text) != null) + if (invalidCharsLengthRegex.find(text) != null) { return FractionParsingError.NUMBER_TOO_LONG + } val fraction = parseFraction(text) return when { fraction == null -> FractionParsingError.INVALID_FORMAT @@ -108,18 +106,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/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 313a5a1f751..6fb6fb73482 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -4,6 +4,24 @@ 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"], 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..8c3ebca13cc --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/FractionParserTest.kt @@ -0,0 +1,280 @@ +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 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) + } +} From 73dccd8f36ce44493c04c3912e76d1bc3c4c2e28 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 26 Jan 2022 19:05:23 -0800 Subject: [PATCH 064/134] Address reviewer comments. --- .../oppia/android/util/math/PolynomialExtensions.kt | 8 +++++--- .../java/org/oppia/android/util/math/RealExtensions.kt | 4 ++++ .../org/oppia/android/util/math/FloatExtensionsTest.kt | 10 ++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index 25bb5f2955d..5983554ddb1 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -24,10 +24,12 @@ fun Polynomial.getConstant(): Real = getTerm(0).coefficient * the polynomial, e.g. "1+x-7x^2"). */ fun Polynomial.toPlainText(): String { - return termList.map { it.toPlainText() }.reduce { acc, termAnswerStr -> + return termList.map { + it.toPlainText() + }.reduce { ongoingPolynomialStr, termAnswerStr -> if (termAnswerStr.startsWith("-")) { - "$acc - ${termAnswerStr.drop(1)}" - } else "$acc + $termAnswerStr" + "$ongoingPolynomialStr - ${termAnswerStr.drop(1)}" + } else "$ongoingPolynomialStr + $termAnswerStr" } } diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index b4fb0a39dad..7f8eabb7901 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -41,6 +41,9 @@ fun Real.toDouble(): Double { * the real (which means proper fractions are converted to improper answer strings since fractions * like '1 1/2' can't be written as a numeric expression without converting them to an improper * form: '3/2'). + * + * Note that this will return an empty string if this [Real] doesn't represent an actual real value + * (e.g. a default instance). */ fun Real.toPlainText(): String = when (realTypeCase) { // Note that the rational part is first converted to an improper fraction since mixed fractions @@ -48,6 +51,7 @@ fun Real.toPlainText(): String = when (realTypeCase) { RATIONAL -> rational.toImproperForm().toAnswerString() IRRATIONAL -> irrational.toPlainString() INTEGER -> integer.toString() + // The Real type isn't valid, so rather than failing just return an empty string. REALTYPE_NOT_SET, null -> "" } diff --git a/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt index 9010828169e..6e1896902e6 100644 --- a/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt @@ -45,6 +45,16 @@ class FloatExtensionsTest { assertThat(leftFloat).isNotEqualTo(rightFloat) } + @Test + fun testFloat_approximatelyEquals_zeroAndNonZeroValue_veryDifferent_returnsFalse() { + val leftFloat = 0f + val rightFloat = 7.3f + + val result = leftFloat.approximatelyEquals(rightFloat) + + assertThat(result).isFalse() + } + @Test fun testFloat_approximatelyEquals_nonZeroValues_outsideInterval_returnsFalse() { val leftFloat = 1.2f From b7535fa9a7172c0f46b075fb99ee0c4987889d34 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 26 Jan 2022 19:06:00 -0800 Subject: [PATCH 065/134] Alphabetize test exemptions. --- scripts/assets/test_file_exemptions.textproto | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index ae6a4f367b2..bd843cca817 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -648,10 +648,10 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/Ko exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/DefineAppLanguageLocaleContext.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt" -exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt" -exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/mockito/MockitoKotlinHelper.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/network/ApiMockLoader.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/network/MockClassroomService.kt" From a00164c9f4b2fcbc9045cf8b37dd85d39c80bbe9 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 26 Jan 2022 19:56:51 -0800 Subject: [PATCH 066/134] Fix typo & add regex check. The new regex check makes it so that all parameterized testing can be more easily tracked by the Android TL. --- .../file_content_validation_checks.textproto | 8 +++++ .../regex/RegexPatternValidationCheckTest.kt | 32 +++++++++++++++++++ .../android/testing/math/TokenSubject.kt | 2 +- 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index fec94690079..2ac733e617b 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -282,3 +282,11 @@ file_content_checks { prohibited_content_regex: "^proto_library\\(" failure_message: "Don't use proto_library. Use oppia_proto_library instead." } +file_content_checks { + file_path_regex: ".+?\\.kt" + prohibited_content_regex: "OppiaParameterizedTestRunner" + failure_message: "To use OppiaParameterizedTestRunner, please add an exemption to file_content_validation_checks.textproto and add an explanation for your use case in your PR description. Note that parameterized tests should only be used in special circumstances where a single behavior can be tested across multiple inputs, or for especially large test suites that can be trivially reduced." + exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" + exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt" + exempted_file_patterns: "testing/src/main/java/org/oppia/android/testing/junit/.+?\\.kt" +} diff --git a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt index 04155481013..0ecea9fbee5 100644 --- a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt +++ b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt @@ -125,6 +125,12 @@ class RegexPatternValidationCheckTest { " null, instead. Delegates uses reflection internally, have a non-trivial initialization" + " cost, and can cause breakages on KitKat devices. See #3939 for more context." private val doNotUseProtoLibrary = "Don't use proto_library. Use oppia_proto_library instead." + private val parameterizedTestRunnerRequiresException = + "To use OppiaParameterizedTestRunner, please add an exemption to" + + " file_content_validation_checks.textproto and add an explanation for your use case in your" + + " PR description. Note that parameterized tests should only be used in special" + + " circumstances where a single behavior can be tested across multiple inputs, or for" + + " especially large test suites that can be trivially reduced." private val wikiReferenceNote = "Refer to https://github.com/oppia/oppia-android/wiki/Static-Analysis-Checks" + "#regexpatternvalidation-check for more details on how to fix this." @@ -1569,6 +1575,32 @@ class RegexPatternValidationCheckTest { ) } + @Test + fun testFileContent_kotlinTestUsesParameterizedTestRunner_fileContentIsNotCorrect() { + val prohibitedContent = + """ + import org.oppia.android.testing.junit.OppiaParameterizedTestRunner + @RunWith(OppiaParameterizedTestRunner::class) + """.trimIndent() + tempFolder.newFolder("testfiles", "domain", "src", "test") + val stringFilePath = "domain/src/test/SomeTest.kt" + tempFolder.newFile("testfiles/$stringFilePath").writeText(prohibitedContent) + + val exception = assertThrows(Exception::class) { + runScript() + } + + assertThat(exception).hasMessageThat().contains(REGEX_CHECK_FAILED_OUTPUT_INDICATOR) + assertThat(outContent.toString().trim()) + .isEqualTo( + """ + $stringFilePath:1: $parameterizedTestRunnerRequiresException + $stringFilePath:2: $parameterizedTestRunnerRequiresException + $wikiReferenceNote + """.trimIndent() + ) + } + @Test fun testFileContent_java8OptionalImport_fileContentIsNotCorrect() { val prohibitedContent = "import java.util.Optional" diff --git a/testing/src/main/java/org/oppia/android/testing/math/TokenSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/TokenSubject.kt index a623c59c7b6..737de61a7b9 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/TokenSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/TokenSubject.kt @@ -131,7 +131,7 @@ class TokenSubject( } /** - * Truth subject for verifying properties of [Token]FunctionName. + * Truth subject for verifying properties of [FunctionName]. * * Call [assertThat] to create the subject. */ From 0287f19afa6067a1fb870f4182b18d339402d4c5 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 13:29:42 -0800 Subject: [PATCH 067/134] Add missing KDocs. --- .../oppia/android/app/parser/FractionParsingUiError.kt | 9 +++++++++ scripts/assets/kdoc_validity_exemptions.textproto | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) 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 index fb1991cca59..731d26e0590 100644 --- a/app/src/main/java/org/oppia/android/app/parser/FractionParsingUiError.kt +++ b/app/src/main/java/org/oppia/android/app/parser/FractionParsingUiError.kt @@ -7,10 +7,19 @@ 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); /** 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" From 1e5279dfe72e7a831e68560ac970ac0308c5c5f0 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 13:45:29 -0800 Subject: [PATCH 068/134] Post-merge cleanups. Also, fix text file exemption ordering. --- .../file_content_validation_checks.textproto | 1 + scripts/assets/test_file_exemptions.textproto | 2 +- .../oppia/android/testing/math/BUILD.bazel | 2 +- .../org/oppia/android/util/math/BUILD.bazel | 20 +++++++++---------- .../org/oppia/android/util/math/BUILD.bazel | 8 ++++---- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index 2ac733e617b..a41099287d7 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -287,6 +287,7 @@ file_content_checks { prohibited_content_regex: "OppiaParameterizedTestRunner" failure_message: "To use OppiaParameterizedTestRunner, please add an exemption to file_content_validation_checks.textproto and add an explanation for your use case in your PR description. Note that parameterized tests should only be used in special circumstances where a single behavior can be tested across multiple inputs, or for especially large test suites that can be trivially reduced." exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" + exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt" exempted_file_patterns: "testing/src/main/java/org/oppia/android/testing/junit/.+?\\.kt" } diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index 8fc1bd8d33a..b17ae7d5abb 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -656,8 +656,8 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/Param exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt" -exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/TokenSubject.kt" diff --git a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel index d60f0f8aafa..f92150592c3 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel @@ -87,7 +87,7 @@ kt_android_library( ":math_expression_subject", ":real_subject", "//third_party:com_google_truth_truth", - "//utility/src/main/java/org/oppia/android/util/math:parsing_error", + "//utility/src/main/java/org/oppia/android/util/math:math_parsing_error", ], ) 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 index bb33e61046d..91b1f83ddf6 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -34,31 +34,31 @@ kt_android_library( ) kt_android_library( - name = "parsing_error", + name = "math_expression_parser", srcs = [ - "MathParsingError.kt", + "MathExpressionParser.kt", ], visibility = [ - "//:oppia_api_visibility", + "//:oppia_testing_visibility", ], deps = [ + ":extensions", + ":math_parsing_error", + ":peekable_iterator", + ":tokenizer", "//model/src/main/proto:math_java_proto_lite", ], ) kt_android_library( - name = "parser", + name = "math_parsing_error", srcs = [ - "MathExpressionParser.kt", + "MathParsingError.kt", ], visibility = [ - "//:oppia_testing_visibility", + "//:oppia_api_visibility", ], deps = [ - ":extensions", - ":parsing_error", - ":peekable_iterator", - ":tokenizer", "//model/src/main/proto:math_java_proto_lite", ], ) 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 index 49a3c4c73fe..b6bebcc07d3 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -19,7 +19,7 @@ oppia_android_test( "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:parser", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) @@ -38,7 +38,7 @@ oppia_android_test( "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:parser", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) @@ -112,7 +112,7 @@ oppia_android_test( "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:parser", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) @@ -151,7 +151,7 @@ oppia_android_test( "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:parser", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) From bbf7e2dfd88760c77f28ac09f3f1c76df4cfbf7e Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 13:48:38 -0800 Subject: [PATCH 069/134] Add new test for negation with math symbol. --- .../util/math/NumericExpressionParserTest.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index b34df9875f6..93628887ae7 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -244,6 +244,21 @@ class NumericExpressionParserTest { } } + @Test + fun testParse_negation_withMathSymbol_returnsExpressionWithUnaryOperation() { + val expression = parseNumericExpressionWithAllErrors("−2") + + assertThat(expression).hasStructureThatMatches { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + @Test fun testParse_positiveUnary_withoutOptionalErrors_returnsExpressionWithUnaryOperation() { val expression = parseNumericExpressionWithoutOptionalErrors("+2") From ba4128c54905b3ea59cb9bc7ed04c54c92044d32 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 14:03:46 -0800 Subject: [PATCH 070/134] Post-merge fixes. --- .../android/util/math/RealExtensionsTest.kt | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt index 2c0b7581c9e..9760b175ff8 100644 --- a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt @@ -11,7 +11,6 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized import org.oppia.android.testing.math.RealSubject.Companion.assertThat -import org.oppia.android.util.math.FractionParser.Companion.parseFraction import org.robolectric.annotation.LooperMode /** Tests for [Real] extensions. */ @@ -57,6 +56,8 @@ class RealExtensionsTest { private val NEGATIVE_PI_REAL = createIrrationalReal(-PI) } + private val fractionParser by lazy { FractionParser() } + @Parameter var lhsInt: Int = Int.MIN_VALUE @Parameter lateinit var lhsFrac: String @Parameter var lhsDouble: Double = Double.MIN_VALUE @@ -508,7 +509,7 @@ class RealExtensionsTest { val result = lhsReal + rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -549,7 +550,7 @@ class RealExtensionsTest { val result = lhsReal + rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -569,7 +570,7 @@ class RealExtensionsTest { val result = lhsReal + rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -690,7 +691,7 @@ class RealExtensionsTest { val result = lhsReal - rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -731,7 +732,7 @@ class RealExtensionsTest { val result = lhsReal - rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -751,7 +752,7 @@ class RealExtensionsTest { val result = lhsReal - rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -872,7 +873,7 @@ class RealExtensionsTest { val result = lhsReal * rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -913,7 +914,7 @@ class RealExtensionsTest { val result = lhsReal * rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -933,7 +934,7 @@ class RealExtensionsTest { val result = lhsReal * rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -1052,7 +1053,7 @@ class RealExtensionsTest { val result = lhsReal / rhsReal // If the divisor doesn't divide the dividend, the result is a fraction. - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -1073,7 +1074,7 @@ class RealExtensionsTest { val result = lhsReal / rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -1114,7 +1115,7 @@ class RealExtensionsTest { val result = lhsReal / rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -1134,7 +1135,7 @@ class RealExtensionsTest { val result = lhsReal / rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -1336,7 +1337,7 @@ class RealExtensionsTest { val result = lhsReal pow rhsReal // Integers raised to a negative integer yields a fraction since x^-y=1/(x^y). - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -1362,7 +1363,7 @@ class RealExtensionsTest { val result = lhsReal pow rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -1423,7 +1424,7 @@ class RealExtensionsTest { val result = lhsReal pow rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -1445,7 +1446,7 @@ class RealExtensionsTest { val result = lhsReal pow rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -1772,15 +1773,15 @@ class RealExtensionsTest { assertThat(result).isIrrationalThat().isWithin(1e-5).of(1.772004515) } + + private fun createRationalReal(rawFractionExpression: String) = + createRationalReal(fractionParser.parseFractionFromString(rawFractionExpression)) } private fun createIntegerReal(value: Int) = Real.newBuilder().apply { integer = value }.build() -private fun createRationalReal(rawFractionExpression: String) = - createRationalReal(parseFraction(rawFractionExpression)) - private fun createRationalReal(value: Fraction) = Real.newBuilder().apply { rational = value }.build() From fd097585daf150d7d141ee560591df5433cb9d9f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 15:17:00 -0800 Subject: [PATCH 071/134] Add KDocs. Also, add new regex exemption for new parameterized tests in this branch. --- .../file_content_validation_checks.textproto | 1 + .../testing/math/MathEquationSubject.kt | 12 ++ .../testing/math/MathExpressionSubject.kt | 33 +++++ .../util/math/ExpressionToLatexConverter.kt | 48 ++++++- .../android/util/math/FractionExtensions.kt | 19 ++- .../util/math/MathExpressionExtensions.kt | 21 ++- .../util/math/NumericExpressionEvaluator.kt | 46 ++++++ .../oppia/android/util/math/RealExtensions.kt | 132 ++++++++++++++++-- .../util/math/NumericExpressionParserTest.kt | 1 + 9 files changed, 295 insertions(+), 18 deletions(-) diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index a41099287d7..8b8fe90e4cd 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -289,5 +289,6 @@ file_content_checks { exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt" + exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt" exempted_file_patterns: "testing/src/main/java/org/oppia/android/testing/junit/.+?\\.kt" } diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt index 1eaa1c821df..859ff36db29 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt @@ -36,9 +36,21 @@ class MathEquationSubject private constructor( */ fun hasRightHandSideThat(): MathExpressionSubject = assertThat(actual.rightSide) + /** + * Returns a [StringSubject] to verify the LaTeX conversion of the tested [MathEquation]. + * + * For more details on LaTeX conversion, see [toRawLatex]. Note that this method, in contrast to + * [convertsWithFractionsToLatexStringThat], retains division operations as-is. + */ fun convertsToLatexStringThat(): StringSubject = assertThat(convertToLatex(divAsFraction = false)) + /** + * Returns a [StringSubject] to verify the LaTeX conversion of the tested [MathEquation]. + * + * For more details on LaTeX conversion, see [toRawLatex]. Note that this method, in contrast to + * [convertsToLatexStringThat], treats divisions as fractions. + */ fun convertsWithFractionsToLatexStringThat(): StringSubject = assertThat(convertToLatex(divAsFraction = true)) diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt index 18e5699675c..1d4298ff3ac 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt @@ -95,18 +95,51 @@ class MathExpressionSubject private constructor( ExpressionComparator.createFromExpression(actual).also(init) } + /** + * Assumes that this expression evaluates to a fraction (i.e. [Real.getRational]) and returns a + * [FractionSubject] to verify the computed value. + * + * Note that this should only be used for numeric expressions as variable expressions cannot be + * evaluated. For more context on expression evaluation, see [evaluateAsNumericExpression]. + */ fun evaluatesToRationalThat(): FractionSubject = FractionSubject.assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.RATIONAL).rational) + /** + * Assumes that this expression evaluates to an irrational (i.e. [Real.getIrrational]) and returns + * a [DoubleSubject] to verify the computed value. + * + * Note that this should only be used for numeric expressions as variable expressions cannot be + * evaluated. For more context on expression evaluation, see [evaluateAsNumericExpression]. + */ fun evaluatesToIrrationalThat(): DoubleSubject = assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.IRRATIONAL).irrational) + /** + * Assumes that this expression evaluates to an integer (i.e. [Real.getInteger]) and returns an + * [IntegerSubject] to verify the computed value. + * + * Note that this should only be used for numeric expressions as variable expressions cannot be + * evaluated. For more context on expression evaluation, see [evaluateAsNumericExpression]. + */ fun evaluatesToIntegerThat(): IntegerSubject = assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.INTEGER).integer) + /** + * Returns a [StringSubject] to verify the LaTeX conversion of the tested [MathExpression]. + * + * For more details on LaTeX conversion, see [toRawLatex]. Note that this method, in contrast to + * [convertsWithFractionsToLatexStringThat], retains division operations as-is. + */ fun convertsToLatexStringThat(): StringSubject = assertThat(convertToLatex(divAsFraction = false)) + /** + * Returns a [StringSubject] to verify the LaTeX conversion of the tested [MathExpression]. + * + * For more details on LaTeX conversion, see [toRawLatex]. Note that this method, in contrast to + * [convertsToLatexStringThat], treats divisions as fractions. + */ fun convertsWithFractionsToLatexStringThat(): StringSubject = assertThat(convertToLatex(divAsFraction = true)) diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToLatexConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToLatexConverter.kt index 2c108f02fa7..05cf8a25e90 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ExpressionToLatexConverter.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToLatexConverter.kt @@ -22,14 +22,33 @@ import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator +/** + * Converter between math equations/expressions and renderable LaTeX strings. + * + * In order to use this converter, directly import [convertToLatex] and call it for any + * [MathExpression]s or [MathEquation]s that should be converted to a renderable LaTeX + * representation. + */ class ExpressionToLatexConverter private constructor() { companion object { - fun MathEquation.convertToLatex(divAsFraction: Boolean): String { - val lhs = leftSide - val rhs = rightSide - return "${lhs.convertToLatex(divAsFraction)} = ${rhs.convertToLatex(divAsFraction)}" - } - + /** + * Returns the LaTeX conversion of this [MathExpression]. + * + * Note that this routine attempts to retain the exact structure of the original expression, but + * not the actual original style. For example, parenthetical groups will be retained but spacing + * between operators will be normalized regardless of the original raw expression. + * + * Note that the returned LaTeX is primarily intended to be render-ready, and may not be as + * nicely human-readable. While some effort is taken to add spacing for better human + * readability, there may be extra curly braces or LaTeX structures to generally ensure + * correct rendering. + * + * Finally, the returned LaTeX should generally be portable/compatible with most LaTeX rendering + * systems as it only relies on basic LaTeX language structures. + * + * @param divAsFraction determines whether divisions within the math structure should be + * rendered instead as fractions rather than division operations + */ fun MathExpression.convertToLatex(divAsFraction: Boolean): String { return when (expressionTypeCase) { CONSTANT -> constant.toPlainText() @@ -47,6 +66,7 @@ class ExpressionToLatexConverter private constructor() { "\\frac{$lhsLatex}{$rhsLatex}" } else "$lhsLatex \\div $rhsLatex" EXPONENTIATE -> "$lhsLatex ^ {$rhsLatex}" + // There's no operator, so try and "recover" by outputting the raw operands. BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> "$lhsLatex $rhsLatex" } @@ -56,6 +76,7 @@ class ExpressionToLatexConverter private constructor() { when (unaryOperation.operator) { NEGATE -> "-$operandLatex" POSITIVE -> "+$operandLatex" + // There's no known operator, so just output the original operand. UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> operandLatex } } @@ -63,12 +84,25 @@ class ExpressionToLatexConverter private constructor() { val argumentLatex = functionCall.argument.convertToLatex(divAsFraction) when (functionCall.functionType) { SQUARE_ROOT -> "\\sqrt{$argumentLatex}" + // There's no recognized function, so try to "recover" by outputting the raw argument. FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> argumentLatex } } GROUP -> "(${group.convertToLatex(divAsFraction)})" - EXPRESSIONTYPE_NOT_SET, null -> "" + EXPRESSIONTYPE_NOT_SET, null -> "" // No corresponding LaTeX, so just go with empty string. } } + + /** + * Returns the LaTeX conversion of this [MathEquation]. + * + * See [convertToLatex] (for [MathExpression]s) for the specific behaviors and expectations of + * this function. + */ + fun MathEquation.convertToLatex(divAsFraction: Boolean): String { + val lhs = leftSide + val rhs = rightSide + return "${lhs.convertToLatex(divAsFraction)} = ${rhs.convertToLatex(divAsFraction)}" + } } } diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index 77e1e7ef420..8a91dff56a2 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -188,7 +188,24 @@ operator fun Fraction.div(rhs: Fraction): Fraction { return this * rhs.toInvertedImproperForm() } -// TODO: document 0^0 case. +/** + * Raises this [Fraction] to the specified [exp] power and returns the result. + * + * Note that since this is an infix operation it should be used as follows (as an example): + * ```kotlin + * val result = fraction pow integerPower + * ``` + * + * This function can only fail when (exceptions are thrown in all cases): + * - This [Fraction] is malformed or incomplete (e.g. a default instance). + * - The resulting [Fraction] would result in a zero denominator. + * + * Some specific details about the returned value: + * - A proper-form fraction is always returned (per [toProperForm]). + * - Negative powers are supported (they will invert the resulting fraction). + * - 0^0 is special-cased to return a 1-valued fraction for consistency with the power function for + * reals (see that KDoc and/or https://stackoverflow.com/a/19955996 for context). + */ infix fun Fraction.pow(exp: Int): Fraction { return when { exp == 0 -> { diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 59b203a0d2d..9da1082f21e 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -6,8 +6,25 @@ import org.oppia.android.app.model.Real import org.oppia.android.util.math.ExpressionToLatexConverter.Companion.convertToLatex import org.oppia.android.util.math.NumericExpressionEvaluator.Companion.evaluate -fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(divAsFraction) - +/** + * Returns the LaTeX conversion of this [MathExpression], with the style configuration determined by + * [divAsFraction]. + * + * See [convertToLatex] for specifics. + */ fun MathExpression.toRawLatex(divAsFraction: Boolean): String = convertToLatex(divAsFraction) +/** + * Returns the LaTeX conversion of this [MathEquation], with the style configuration determined by + * [divAsFraction]. + * + * See [convertToLatex] for specifics. + */ +fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(divAsFraction) + +/** + * Returns the [Real] evaluation of this [MathExpression]. + * + * See [evaluate] for specifics. + */ fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() diff --git a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionEvaluator.kt b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionEvaluator.kt index 766da590400..1b314675ea7 100644 --- a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionEvaluator.kt +++ b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionEvaluator.kt @@ -25,8 +25,54 @@ import org.oppia.android.app.model.Real import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator +/** + * Numeric evaluator for numeric [MathExpression]s. + * + * In order to use this evaluator, directly import [evaluate] and call it for any numeric + * [MathExpression]s that should be evaluated. + */ class NumericExpressionEvaluator private constructor() { companion object { + /** + * Evaluates a math expression. + * + * This function only works with numeric expressions since variable expressions have no means + * for evaluation (so they'll always result in a ``null`` return value). + * + * The function generally attempts to retain the most precise representation of a value in the + * following order (from highest priority to lowest): + * 1. Integers + * 2. Fractions (rational values) + * 3. Doubles (irrational values) + * + * Doubles will only be used if there's no other choice as they do not have perfect precision + * unlike the other two structures. Further, it's possible for doubles to be used in cases where + * an integer could work, or fractions to represent whole integers (due to quirks in underlying + * routines). That being said, within a certain precision threshold values returned by this + * function should be deterministic across multiple calls (for the same [MathExpression]). + * + * There are a number of cases where this function will fail: + * - When trying to evaluate a variable expression. + * - When trying to evaluate an invalid [MathExpression] (i.e. one of the substructures within + * the expression is not actually initialized per the proto structures). + * - When trying to perform an impossible math operation (such as divide by zero). Note that + * this will sometimes result in a [Real] being returned with a value like NaN or infinity, + * and other times may result in an exception being thrown. + * + * Note that there's no guard against overflowing values during computation, so care should be + * taken by the caller that this is possible for certain expressions. + * + * For more specifics on the constituent operations that "power" this function, see: + * - [Real.plus] + * - [Real.minus] + * - [Real.times] + * - [Real.div] + * - [Real.pow] + * - [Real.unaryMinus] + * - [sqrt] + * + * @return the [Real] representing the evaluated expression, or ``null`` if something went wrong + */ fun MathExpression.evaluate(): Real? { return when (expressionTypeCase) { CONSTANT -> constant diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 6e8c01f33f4..adb977396f5 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -16,6 +16,12 @@ import kotlin.math.pow */ fun Real.isRational(): Boolean = realTypeCase == RATIONAL +/** + * Returns whether this [Real] is explicitly an integer. + * + * This returns false if the real is a rational despite that being mathematically an integer (e.g. a + * whole number fraction). + */ fun Real.isInteger(): Boolean = realTypeCase == INTEGER /** Returns whether this [Real] is negative. */ @@ -81,6 +87,23 @@ operator fun Real.unaryMinus(): Real { } } +/** + * Adds this [Real] with another and returns the result. + * + * Neither [Real] being added are changed during the operation. + * + * Note that this function will always succeed (unless one of the [Real]s is malformed or + * incomplete), but the type of [Real] that's returned depends on the constituent [Real]s being + * added. For reference, here's how the conversion behaves: + * + * |---------------------------------------------------| + * | + | integer | rational | irrational | + * |------------|------------|------------|------------| + * | integer | integer | rational | irrational | + * | rational | rational | rational | irrational | + * | irrational | irrational | irrational | irrational | + * |---------------------------------------------------| + */ operator fun Real.plus(rhs: Real): Real { return combine( this, rhs, Fraction::plus, Fraction::plus, Fraction::plus, Double::plus, Double::plus, @@ -88,6 +111,15 @@ operator fun Real.plus(rhs: Real): Real { ) } +/** + * Subtracts this [Real] from another and returns the result. + * + * Neither [Real] being subtracted are changed during the operation. + * + * Note that this function will always succeed (unless one of the [Real]s is malformed or + * incomplete), but the type of [Real] that's returned depends on the constituent [Real]s being + * subtracted. For reference, see [Real.plus] (the same type conversion is used). + */ operator fun Real.minus(rhs: Real): Real { return combine( this, rhs, Fraction::minus, Fraction::minus, Fraction::minus, Double::minus, Double::minus, @@ -95,6 +127,18 @@ operator fun Real.minus(rhs: Real): Real { ) } +/** + * Multiplies this [Real] with another and returns the result. + * + * Neither [Real] being multiplied are changed during the operation. + * + * Note that this function will always succeed (unless one of the [Real]s is malformed or + * incomplete), but the type of [Real] that's returned depends on the constituent [Real]s being + * multiplied. For reference, see [Real.plus] (the same type conversion is used). + * + * Note that effective divisions by zero (i.e. fractions with zero denominators) may result in + * either an infinity being returned or an exception being thrown. + */ operator fun Real.times(rhs: Real): Real { return combine( this, rhs, Fraction::times, Fraction::times, Fraction::times, Double::times, Double::times, @@ -102,6 +146,18 @@ operator fun Real.times(rhs: Real): Real { ) } +/** + * Divides this [Real] by another and returns the result. + * + * Neither [Real] being divided are changed during the operation. + * + * Note that this function will always succeed (unless one of the [Real]s is malformed or + * incomplete), but the type of [Real] that's returned depends on the constituent [Real]s being + * divided. For reference, see [Real.plus] (the same type conversion is used). + * + * Note also that divisions by zero may result in either an exception being thrown, or an infinity + * being returned. + */ operator fun Real.div(rhs: Real): Real { return combine( this, rhs, Fraction::div, Fraction::div, Fraction::div, Double::div, Double::div, Double::div, @@ -109,14 +165,49 @@ operator fun Real.div(rhs: Real): Real { ) } -// TODO: document that roots represents the real value representation vs. principal root. Also, -// document 0^0 case per https://stackoverflow.com/a/19955996. -// Rules: -// - Anything involving a double always becomes a double. -// - Int^Int stays int unless it's negative (then it becomes a fraction) -// - Int^Fraction is treated as a fraction power & root (it becomes fraction or double) -// - Fraction^Int always yields a fraction -// - Fraction^Fraction yields a fraction or double (depending on the denominator root) +/** + * Computes the power of this [Real] raised to [rhs] and returns the result. + * + * Neither [Real] being combined are changed during the operation. + * + * As this is an infix function, it should be called as so (example): + * ```kotlin + * val result = baseReal pow powerReal + * ``` + * + * This function can fail in a few circumstances: + * - One of the [Real]s is malformed or incomplete (such as a default instance). + * - In cases where a root is being taken (i.e. when |[rhs]| < 1), if the root cannot be taken + * either an exception will be thrown or NaN will be returned (such as trying to take the even + * root of a negative value). + * + * Further, note that this function represents the real value root rather than the principal root, + * so negative bases are allowed so long as the root being used is odd. For non-integerlike powers, + * the base should never be negative except for fractions that could result in a positive base after + * exponentiation. + * + * This function special cases 0^0 to return 1 in all cases for consistency with the system ``pow`` + * function and other languages, per: https://stackoverflow.com/a/19955996. + * + * Finally, this function also attempts to retain maximum precision in much the same way as [sqrt] + * and [Real.plus] except there are more cases when a value may change types. See the following + * table for reference: + * + * |----------------------------------------------------------------------------------------------| + * | pow | positive int | negative int | rootable rational* | other rationals | irrational | + * |------------|--------------|--------------|--------------------|-----------------|------------| + * | integer | integer | rational | rational | irrational | irrational | + * | rational | rational | rational | rational | irrational | irrational | + * | irrational | irrational | irrational | irrational | irrational | irrational | + * |----------------------------------------------------------------------------------------------| + * + * *This corresponds to fraction powers whose denominator (which are treated as roots) can perform a + * perfect square root of either the integer base (for integer [Real]s) or both the numerator and + * denominator integers (for rational [Real]s). + * + * (Note that the left column represents the left-hand side and the top row represents the + * right-hand side of the operation). + */ infix fun Real.pow(rhs: Real): Real { // Powers can really only be effectively done via floats or whole-number only fractions. return when (realTypeCase) { @@ -160,6 +251,31 @@ infix fun Real.pow(rhs: Real): Real { } } +/** + * Returns the square root of the specified [Real]. + * + * [real] is not changed as a result of this operation (a new [Real] value is returned). + * + * Failure cases: + * - An invalid [Real] is passed in (such as a default instance), resulting in an exception being + * thrown. + * - A negative value is passed in (this will either result in an exception or a NaN being + * returned). + * + * Similar to [Real.plus] & other operations, this function attempts to retain as much precision as + * possible by first performing perfect roots before needing to perform a numerical approximation. + * This is achieved by attempting to take perfect integer roots for integer and rational types and, + * if that's not possible, then converting to a double. See the following conversion table for + * reference: + * + * |------------------------------------------------| + * | sqrt | perfect square | all other values | + * |------------|----------------|------------------| + * | integer | integer | irrational | + * | rational | rational | irrational | + * | irrational | irrational | irrational | + * |------------------------------------------------| + */ fun sqrt(real: Real): Real { return when (real.realTypeCase) { RATIONAL -> sqrt(real.rational) diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index a1557bc1745..cf43a7b3955 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -286,6 +286,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(-2) } @Test From 01c2326add92c975fa6f79d6df72578e9d84c3ad Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 15:43:51 -0800 Subject: [PATCH 072/134] Refactor & simplify real ext impl. Also, fix/clarify some KDocs. --- .../oppia/android/util/math/RealExtensions.kt | 240 +++++++++--------- 1 file changed, 114 insertions(+), 126 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index adb977396f5..99434f5e47a 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -80,9 +80,9 @@ fun Real.isApproximatelyEqualTo(value: Double): Boolean { */ operator fun Real.unaryMinus(): Real { return when (realTypeCase) { - RATIONAL -> recompute { it.setRational(-rational) } - IRRATIONAL -> recompute { it.setIrrational(-irrational) } - INTEGER -> recompute { it.setInteger(-integer) } + RATIONAL -> createRationalReal(-rational) + IRRATIONAL -> createIrrationalReal(-irrational) + INTEGER -> createIntegerReal(-integer) REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $this.") } } @@ -103,12 +103,34 @@ operator fun Real.unaryMinus(): Real { * | rational | rational | rational | irrational | * | irrational | irrational | irrational | irrational | * |---------------------------------------------------| + * + * As indicated by the above table, this function attempts to maintain as much precision as possible + * during operations (but will fall back to [Double]s if the calculation would otherwise result in a + * high level of error). While [Double]s don't perfectly capture precision, their error levels are + * generally better than the rounding errors encountered from integer arithmetic. */ operator fun Real.plus(rhs: Real): Real { - return combine( - this, rhs, Fraction::plus, Fraction::plus, Fraction::plus, Double::plus, Double::plus, - Double::plus, Int::plus, Int::plus, Int::add - ) + return when (realTypeCase) { + RATIONAL -> when (rhs.realTypeCase) { + RATIONAL -> createRationalReal(rational + rhs.rational) + IRRATIONAL -> createIrrationalReal(rational.toDouble() + rhs.irrational) + INTEGER -> createRationalReal(rational + rhs.integer.toWholeNumberFraction()) + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") + } + IRRATIONAL -> when (rhs.realTypeCase) { + RATIONAL -> createIrrationalReal(irrational + rhs.rational.toDouble()) + IRRATIONAL -> createIrrationalReal(irrational + rhs.irrational) + INTEGER -> createIrrationalReal(irrational + rhs.integer) + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") + } + INTEGER -> when (rhs.realTypeCase) { + RATIONAL -> createRationalReal(integer.toWholeNumberFraction() + rhs.rational) + IRRATIONAL -> createIrrationalReal(integer + rhs.irrational) + INTEGER -> createIntegerReal(integer + rhs.integer) + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") + } + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $this.") + } } /** @@ -121,10 +143,27 @@ operator fun Real.plus(rhs: Real): Real { * subtracted. For reference, see [Real.plus] (the same type conversion is used). */ operator fun Real.minus(rhs: Real): Real { - return combine( - this, rhs, Fraction::minus, Fraction::minus, Fraction::minus, Double::minus, Double::minus, - Double::minus, Int::minus, Int::minus, Int::subtract - ) + return when (realTypeCase) { + RATIONAL -> when (rhs.realTypeCase) { + RATIONAL -> createRationalReal(rational - rhs.rational) + IRRATIONAL -> createIrrationalReal(rational.toDouble() - rhs.irrational) + INTEGER -> createRationalReal(rational - rhs.integer.toWholeNumberFraction()) + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") + } + IRRATIONAL -> when (rhs.realTypeCase) { + RATIONAL -> createIrrationalReal(irrational - rhs.rational.toDouble()) + IRRATIONAL -> createIrrationalReal(irrational - rhs.irrational) + INTEGER -> createIrrationalReal(irrational - rhs.integer) + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") + } + INTEGER -> when (rhs.realTypeCase) { + RATIONAL -> createRationalReal(integer.toWholeNumberFraction() - rhs.rational) + IRRATIONAL -> createIrrationalReal(integer - rhs.irrational) + INTEGER -> createIntegerReal(integer - rhs.integer) + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") + } + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $this.") + } } /** @@ -140,10 +179,27 @@ operator fun Real.minus(rhs: Real): Real { * either an infinity being returned or an exception being thrown. */ operator fun Real.times(rhs: Real): Real { - return combine( - this, rhs, Fraction::times, Fraction::times, Fraction::times, Double::times, Double::times, - Double::times, Int::times, Int::times, Int::multiply - ) + return when (realTypeCase) { + RATIONAL -> when (rhs.realTypeCase) { + RATIONAL -> createRationalReal(rational * rhs.rational) + IRRATIONAL -> createIrrationalReal(rational.toDouble() * rhs.irrational) + INTEGER -> createRationalReal(rational * rhs.integer.toWholeNumberFraction()) + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") + } + IRRATIONAL -> when (rhs.realTypeCase) { + RATIONAL -> createIrrationalReal(irrational * rhs.rational.toDouble()) + IRRATIONAL -> createIrrationalReal(irrational * rhs.irrational) + INTEGER -> createIrrationalReal(irrational * rhs.integer) + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") + } + INTEGER -> when (rhs.realTypeCase) { + RATIONAL -> createRationalReal(integer.toWholeNumberFraction() * rhs.rational) + IRRATIONAL -> createIrrationalReal(integer * rhs.irrational) + INTEGER -> createIntegerReal(integer * rhs.integer) + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") + } + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $this.") + } } /** @@ -153,16 +209,35 @@ operator fun Real.times(rhs: Real): Real { * * Note that this function will always succeed (unless one of the [Real]s is malformed or * incomplete), but the type of [Real] that's returned depends on the constituent [Real]s being - * divided. For reference, see [Real.plus] (the same type conversion is used). + * divided. For reference, see [Real.plus] for type conversion. It's the same for this method except + * one case: integer divided by integers. If the division is perfect (e.g. 4/2), an integer will be + * returned. Otherwise, a rational [Fraction] will be returned. * * Note also that divisions by zero may result in either an exception being thrown, or an infinity * being returned. */ operator fun Real.div(rhs: Real): Real { - return combine( - this, rhs, Fraction::div, Fraction::div, Fraction::div, Double::div, Double::div, Double::div, - Int::div, Int::div, Int::divide - ) + return when (realTypeCase) { + RATIONAL -> when (rhs.realTypeCase) { + RATIONAL -> createRationalReal(rational / rhs.rational) + IRRATIONAL -> createIrrationalReal(rational.toDouble() / rhs.irrational) + INTEGER -> createRationalReal(rational / rhs.integer.toWholeNumberFraction()) + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") + } + IRRATIONAL -> when (rhs.realTypeCase) { + RATIONAL -> createIrrationalReal(irrational / rhs.rational.toDouble()) + IRRATIONAL -> createIrrationalReal(irrational / rhs.irrational) + INTEGER -> createIrrationalReal(irrational / rhs.integer) + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") + } + INTEGER -> when (rhs.realTypeCase) { + RATIONAL -> createRationalReal(integer.toWholeNumberFraction() / rhs.rational) + IRRATIONAL -> createIrrationalReal(integer / rhs.irrational) + INTEGER -> integer.divide(rhs.integer) + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") + } + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $this.") + } } /** @@ -218,17 +293,17 @@ infix fun Real.pow(rhs: Real): Real { RATIONAL -> rhs.rational.toImproperForm().let { power -> (rational pow power.numerator).root(power.denominator, power.isNegative) } - IRRATIONAL -> recompute { it.setIrrational(rational.pow(rhs.irrational)) } - INTEGER -> recompute { it.setRational(rational pow rhs.integer) } + IRRATIONAL -> createIrrationalReal(rational.toDouble().pow(rhs.irrational)) + INTEGER -> createRationalReal(rational pow rhs.integer) REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") } } IRRATIONAL -> { // Left-hand side is a double. when (rhs.realTypeCase) { - RATIONAL -> recompute { it.setIrrational(irrational.pow(rhs.rational)) } - IRRATIONAL -> recompute { it.setIrrational(irrational.pow(rhs.irrational)) } - INTEGER -> recompute { it.setIrrational(irrational.pow(rhs.integer)) } + RATIONAL -> createIrrationalReal(irrational.pow(rhs.rational.toDouble())) + IRRATIONAL -> createIrrationalReal(irrational.pow(rhs.irrational)) + INTEGER -> createIrrationalReal(irrational.pow(rhs.integer)) REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") } } @@ -242,7 +317,7 @@ infix fun Real.pow(rhs: Real): Real { power.denominator, power.isNegative ) } - IRRATIONAL -> recompute { it.setIrrational(integer.toDouble().pow(rhs.irrational)) } + IRRATIONAL -> createIrrationalReal(integer.toDouble().pow(rhs.irrational)) INTEGER -> integer.pow(rhs.integer) REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") } @@ -278,9 +353,9 @@ infix fun Real.pow(rhs: Real): Real { */ fun sqrt(real: Real): Real { return when (real.realTypeCase) { - RATIONAL -> sqrt(real.rational) - IRRATIONAL -> real.recompute { it.setIrrational(kotlin.math.sqrt(real.irrational)) } - INTEGER -> sqrt(real.integer) + RATIONAL -> real.rational.root(base = 2, invert = false) + IRRATIONAL -> createIrrationalReal(kotlin.math.sqrt(real.irrational)) + INTEGER -> root(real.integer, base = 2) REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $real.") } } @@ -292,30 +367,6 @@ fun sqrt(real: Real): Real { */ fun abs(real: Real): Real = if (real.isNegative()) -real else real -private operator fun Double.plus(rhs: Fraction): Double = this + rhs.toDouble() -private operator fun Fraction.plus(rhs: Double): Double = toDouble() + rhs -private operator fun Fraction.plus(rhs: Int): Fraction = this + rhs.toWholeNumberFraction() -private operator fun Int.plus(rhs: Fraction): Fraction = toWholeNumberFraction() + rhs -private operator fun Double.minus(rhs: Fraction): Double = this - rhs.toDouble() -private operator fun Fraction.minus(rhs: Double): Double = toDouble() - rhs -private operator fun Fraction.minus(rhs: Int): Fraction = this - rhs.toWholeNumberFraction() -private operator fun Int.minus(rhs: Fraction): Fraction = toWholeNumberFraction() - rhs -private operator fun Double.times(rhs: Fraction): Double = this * rhs.toDouble() -private operator fun Fraction.times(rhs: Double): Double = toDouble() * rhs -private operator fun Fraction.times(rhs: Int): Fraction = this * rhs.toWholeNumberFraction() -private operator fun Int.times(rhs: Fraction): Fraction = toWholeNumberFraction() * rhs -private operator fun Double.div(rhs: Fraction): Double = this / rhs.toDouble() -private operator fun Fraction.div(rhs: Double): Double = toDouble() / rhs -private operator fun Fraction.div(rhs: Int): Fraction = this / rhs.toWholeNumberFraction() -private operator fun Int.div(rhs: Fraction): Fraction = toWholeNumberFraction() / rhs - -private fun Int.add(rhs: Int): Real = Real.newBuilder().apply { integer = this@add + rhs }.build() -private fun Int.subtract(rhs: Int): Real = Real.newBuilder().apply { - integer = this@subtract - rhs -}.build() -private fun Int.multiply(rhs: Int): Real = Real.newBuilder().apply { - integer = this@multiply * rhs -}.build() private fun Int.divide(rhs: Int): Real = Real.newBuilder().apply { // If rhs divides this integer, retain the integer. val lhs = this@divide @@ -331,9 +382,6 @@ private fun Int.divide(rhs: Int): Real = Real.newBuilder().apply { } }.build() -private fun Double.pow(rhs: Fraction): Double = this.pow(rhs.toDouble()) -private fun Fraction.pow(rhs: Double): Double = toDouble().pow(rhs) - private fun Int.pow(exp: Int): Real { return when { exp == 0 -> Real.newBuilder().apply { integer = 1 }.build() @@ -348,8 +396,6 @@ private fun Int.pow(exp: Int): Real { } } -private fun sqrt(fraction: Fraction): Real = fraction.root(base = 2, invert = false) - private fun Fraction.root(base: Int, invert: Boolean): Real { check(base > 0) { "Expected base of 1 or higher, not: $base" } @@ -376,8 +422,6 @@ private fun Fraction.root(base: Int, invert: Boolean): Real { } } -private fun sqrt(int: Int): Real = root(int, base = 2) - private fun root(int: Int, base: Int): Real { // First, check if the integer is a root. Base reference for possible methods: // https://www.researchgate.net/post/How-to-decide-if-a-given-number-will-have-integer-square-root-or-not. @@ -440,70 +484,14 @@ private fun root(int: Int, base: Int): Real { private fun Int.isOdd() = this % 2 == 1 -private fun Real.recompute(transform: (Real.Builder) -> Real.Builder): Real { - return transform(newBuilderForType()).build() -} +private fun createRationalReal(value: Fraction): Real = Real.newBuilder().apply { + rational = value +}.build() -// TODO: consider replacing this with inline alternatives since they'll probably be simpler. -private fun combine( - lhs: Real, - rhs: Real, - leftRationalRightRationalOp: (Fraction, Fraction) -> Fraction, - leftRationalRightIrrationalOp: (Fraction, Double) -> Double, - leftRationalRightIntegerOp: (Fraction, Int) -> Fraction, - leftIrrationalRightRationalOp: (Double, Fraction) -> Double, - leftIrrationalRightIrrationalOp: (Double, Double) -> Double, - leftIrrationalRightIntegerOp: (Double, Int) -> Double, - leftIntegerRightRationalOp: (Int, Fraction) -> Fraction, - leftIntegerRightIrrationalOp: (Int, Double) -> Double, - leftIntegerRightIntegerOp: (Int, Int) -> Real, -): Real { - return when (lhs.realTypeCase) { - RATIONAL -> { - // Left-hand side is Fraction. - when (rhs.realTypeCase) { - RATIONAL -> - lhs.recompute { it.setRational(leftRationalRightRationalOp(lhs.rational, rhs.rational)) } - IRRATIONAL -> - lhs.recompute { - it.setIrrational(leftRationalRightIrrationalOp(lhs.rational, rhs.irrational)) - } - INTEGER -> - lhs.recompute { it.setRational(leftRationalRightIntegerOp(lhs.rational, rhs.integer)) } - REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") - } - } - IRRATIONAL -> { - // Left-hand side is a double. - when (rhs.realTypeCase) { - RATIONAL -> - lhs.recompute { - it.setIrrational(leftIrrationalRightRationalOp(lhs.irrational, rhs.rational)) - } - IRRATIONAL -> - lhs.recompute { - it.setIrrational(leftIrrationalRightIrrationalOp(lhs.irrational, rhs.irrational)) - } - INTEGER -> - lhs.recompute { - it.setIrrational(leftIrrationalRightIntegerOp(lhs.irrational, rhs.integer)) - } - REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") - } - } - INTEGER -> { - // Left-hand side is an integer. - when (rhs.realTypeCase) { - RATIONAL -> - lhs.recompute { it.setRational(leftIntegerRightRationalOp(lhs.integer, rhs.rational)) } - IRRATIONAL -> - lhs.recompute { - it.setIrrational(leftIntegerRightIrrationalOp(lhs.integer, rhs.irrational)) - } - INTEGER -> leftIntegerRightIntegerOp(lhs.integer, rhs.integer) - REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") - } - } - REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $lhs.") - } -} +private fun createIrrationalReal(value: Double): Real = Real.newBuilder().apply { + irrational = value +}.build() + +private fun createIntegerReal(value: Int): Real = Real.newBuilder().apply { + integer = value +}.build() From 98d6939803f3e2cc7f952332e23f15edb81ec6b5 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 16:13:26 -0800 Subject: [PATCH 073/134] Lint fixes. --- .../java/org/oppia/android/util/math/FractionExtensions.kt | 1 - .../main/java/org/oppia/android/util/math/RealExtensions.kt | 2 +- .../android/util/math/ExpressionToLatexConverterTest.kt | 5 +++-- .../android/util/math/NumericExpressionEvaluatorTest.kt | 3 ++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index 8a91dff56a2..bd0c8093ede 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -236,7 +236,6 @@ fun Int.toWholeNumberFraction(): Fraction { }.build() } - /** Returns the greatest common divisor between two integers. */ private fun gcd(x: Int, y: Int): Int { return if (y == 0) x else gcd(y, x % y) diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 99434f5e47a..cc3f3687e91 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -1,12 +1,12 @@ package org.oppia.android.util.math -import kotlin.math.absoluteValue import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.Real import org.oppia.android.app.model.Real.RealTypeCase.INTEGER import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET +import kotlin.math.absoluteValue import kotlin.math.pow /** diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt index 87be7c78451..1af737e0583 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt @@ -188,9 +188,10 @@ class ExpressionToLatexConverterTest { ErrorCheckingMode.ALL_ERRORS ).getExpectedSuccess() } - + private fun parseNumericExpressionInternal( - expression: String, errorCheckingMode: ErrorCheckingMode + expression: String, + errorCheckingMode: ErrorCheckingMode ): MathExpression { return MathExpressionParser.parseNumericExpression( expression, errorCheckingMode diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionEvaluatorTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionEvaluatorTest.kt index a16afda5a0e..1f4786bde4b 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionEvaluatorTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionEvaluatorTest.kt @@ -218,7 +218,8 @@ class NumericExpressionEvaluatorTest { private companion object { private fun parseNumericExpression( - expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + expression: String, + errorCheckingMode: ErrorCheckingMode = ALL_ERRORS ): MathExpression { return MathExpressionParser.parseNumericExpression( expression, errorCheckingMode From ca412f7dcb2d24ce9af1bd884a73a0d8f450e685 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 17:32:39 -0800 Subject: [PATCH 074/134] Simplify operation list converter a lot. This inlines three recursive operations to be done during the actual computation to simplify the overall converter complexity (and to make determining the test matrix easier). --- ...ssionToComparableOperationListConverter.kt | 160 +++++++----------- .../util/math/MathExpressionExtensions.kt | 35 +--- .../oppia/android/util/math/RealExtensions.kt | 1 + 3 files changed, 60 insertions(+), 136 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt index 4cfa9acec66..ef1008cab54 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt @@ -2,17 +2,15 @@ package org.oppia.android.util.math import org.oppia.android.app.model.ComparableOperationList import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation -import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType -import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.ACCUMULATION_TYPE_UNSPECIFIED import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.PRODUCT import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.SUMMATION import org.oppia.android.app.model.ComparableOperationList.ComparableOperation -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.COMPARISONTYPE_NOT_SET import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.CONSTANT_TERM import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.NON_COMMUTATIVE_OPERATION import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.VARIABLE_TERM import org.oppia.android.app.model.ComparableOperationList.NonCommutativeOperation import org.oppia.android.app.model.ComparableOperationList.NonCommutativeOperation.OperationTypeCase +import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE @@ -28,13 +26,15 @@ import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERA import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE import org.oppia.android.app.model.MathFunctionCall.FunctionType import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE -import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator -import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator class ExpressionToComparableOperationListConverter private constructor() { companion object { + // TODO: consider eliminating the comparator extensions. Probably should verify full test suite + // & the old tests before deleting the old tests. + private val COMPARABLE_OPERATION_COMPARATOR: Comparator by lazy { // Some of the comparators must be deferred since they indirectly reference this comparator // (which isn't valid until it's fully assembled). @@ -87,9 +87,9 @@ class ExpressionToComparableOperationListConverter private constructor() { ) } - fun MathExpression.toComparable(): ComparableOperationList { + fun MathExpression.toComparableOperationList(): ComparableOperationList { return ComparableOperationList.newBuilder().apply { - rootOperation = toComparableOperation().stabilizeNegation().sort() + rootOperation = toComparableOperation() }.build() } @@ -137,6 +137,7 @@ class ExpressionToComparableOperationListConverter private constructor() { accumulationType = SUMMATION addOperationToSum(binaryOperation.leftOperand, forceNegative = false) addOperationToSum(binaryOperation.rightOperand, forceNegative = isRhsNegative) + sort() }.build() }.build() } @@ -145,8 +146,12 @@ class ExpressionToComparableOperationListConverter private constructor() { return ComparableOperation.newBuilder().apply { commutativeAccumulation = CommutativeAccumulation.newBuilder().apply { accumulationType = PRODUCT - addOperationToProduct(binaryOperation.leftOperand, forceInverse = false) - addOperationToProduct(binaryOperation.rightOperand, forceInverse = isRhsInverted) + val negativeCount = + addOperationToProduct(binaryOperation.leftOperand, forceInverse = false) + + addOperationToProduct(binaryOperation.rightOperand, forceInverse = isRhsInverted) + // If an odd number of terms were negative then the overall product is negative. + isNegated = (negativeCount % 2) != 0 + sort() }.build() }.build() } @@ -165,32 +170,61 @@ class ExpressionToComparableOperationListConverter private constructor() { addOperationToSum(expression.binaryOperation.leftOperand, forceNegative) addOperationToSum(expression.binaryOperation.rightOperand, forceNegative = true) } - else -> if (forceNegative) { - addCombinedOperations(expression.toComparableOperation().makeNegative()) - } else addCombinedOperations(expression.toComparableOperation()) + else -> when { + // Skip groups so that nested operations can be properly combined. + expression.expressionTypeCase == GROUP -> + addOperationToSum(expression.group, forceNegative) + forceNegative -> addCombinedOperations(expression.toComparableOperation().makeNegative()) + else -> addCombinedOperations(expression.toComparableOperation()) + } } } + /** + * Recursively adds [expression] tp the ongoing product [CommutativeAccumulation.Builder] by + * collapsing subsequent products into a single list. + * + * @param forceInverse whether this expression is being divided rather than multiplied + * @return the number of negative operations that were made positive before being added to the + * accumulation + */ private fun CommutativeAccumulation.Builder.addOperationToProduct( expression: MathExpression, forceInverse: Boolean - ) { - when (expression.binaryOperation.operator) { - MULTIPLY -> { + ): Int { + return when { + expression.binaryOperation.operator == MULTIPLY -> { // If the whole operation is inverted, carry it to the left-hand side of the operation. - addOperationToProduct(expression.binaryOperation.leftOperand, forceInverse) - addOperationToProduct(expression.binaryOperation.rightOperand, forceInverse = false) + addOperationToProduct(expression.binaryOperation.leftOperand, forceInverse) + + addOperationToProduct(expression.binaryOperation.rightOperand, forceInverse = false) + } + expression.binaryOperation.operator == DIVIDE -> { + addOperationToProduct(expression.binaryOperation.leftOperand, forceInverse) + + addOperationToProduct(expression.binaryOperation.rightOperand, forceInverse = true) } - DIVIDE -> { - addOperationToProduct(expression.binaryOperation.leftOperand, forceInverse) - addOperationToProduct(expression.binaryOperation.rightOperand, forceInverse = true) + // Skip groups so that nested operations can be properly combined. + expression.expressionTypeCase == GROUP -> + addOperationToProduct(expression.group, forceInverse) + else -> { + val operationExpression = expression.toComparableOperation() + val positiveConvertedOperation = operationExpression.makePositive() + if (forceInverse) { + addCombinedOperations(positiveConvertedOperation.makeInverted()) + } else addCombinedOperations(positiveConvertedOperation) + if (operationExpression.isNegated) 1 else 0 } - else -> if (forceInverse) { - addCombinedOperations(expression.toComparableOperation().makeInverted()) - } else addCombinedOperations(expression.toComparableOperation()) } } + private fun CommutativeAccumulation.Builder.sort() { + // Replace the list operations with a sorted list of operations. Note that the inner elements + // are already sorted since this is called during operation creation time (so nested + // operations would have already been sorted). + val operationsList = combinedOperationsList.toMutableList() + clearCombinedOperations() + addAllCombinedOperations(operationsList.sortedWith(COMPARABLE_OPERATION_COMPARATOR)) + } + private fun MathExpression.toNonCommutativeOperation( setOperation: NonCommutativeOperation.Builder.( NonCommutativeOperation.BinaryOperation @@ -216,85 +250,5 @@ class ExpressionToComparableOperationListConverter private constructor() { private fun ComparableOperation.makeInverted(): ComparableOperation = toBuilder().apply { isInverted = true }.build() - - private fun ComparableOperation.stabilizeNegation(): ComparableOperation { - return when (comparisonTypeCase) { - ComparableOperation.ComparisonTypeCase.COMMUTATIVE_ACCUMULATION -> { - val stabilizedOperations = - commutativeAccumulation.combinedOperationsList.map { it.stabilizeNegation() } - when (commutativeAccumulation.accumulationType) { - SUMMATION -> toBuilder().apply { - commutativeAccumulation = commutativeAccumulation.toBuilder().apply { - clearCombinedOperations() - addAllCombinedOperations(stabilizedOperations) - }.build() - }.build() - PRODUCT -> { - // Negations can be combined for all constituent operations & brought up to the - // top-level operation. - val negativeCount = stabilizedOperations.count { - it.isNegated - } + if (isNegated) 1 else 0 - val positiveOperations = stabilizedOperations.map { it.makePositive() } - toBuilder().apply { - isNegated = (negativeCount % 2) == 1 - commutativeAccumulation = commutativeAccumulation.toBuilder().apply { - clearCombinedOperations() - addAllCombinedOperations(positiveOperations) - }.build() - }.build() - } - ACCUMULATION_TYPE_UNSPECIFIED, AccumulationType.UNRECOGNIZED, null -> this - } - } - NON_COMMUTATIVE_OPERATION -> toBuilder().apply { - // Negation can't be extracted from commutative operations. - nonCommutativeOperation = when (nonCommutativeOperation.operationTypeCase) { - OperationTypeCase.EXPONENTIATION -> nonCommutativeOperation.toBuilder().apply { - exponentiation = nonCommutativeOperation.exponentiation.toBuilder().apply { - leftOperand = nonCommutativeOperation.exponentiation.leftOperand.stabilizeNegation() - rightOperand = - nonCommutativeOperation.exponentiation.rightOperand.stabilizeNegation() - }.build() - }.build() - OperationTypeCase.SQUARE_ROOT -> nonCommutativeOperation.toBuilder().apply { - squareRoot = nonCommutativeOperation.squareRoot.stabilizeNegation() - }.build() - OperationTypeCase.OPERATIONTYPE_NOT_SET, null -> nonCommutativeOperation - } - }.build() - CONSTANT_TERM -> this - VARIABLE_TERM -> this - COMPARISONTYPE_NOT_SET, null -> this - } - } - - private fun ComparableOperation.sort(): ComparableOperation { - return when (comparisonTypeCase) { - ComparableOperation.ComparisonTypeCase.COMMUTATIVE_ACCUMULATION -> toBuilder().apply { - commutativeAccumulation = commutativeAccumulation.toBuilder().apply { - clearCombinedOperations() - // Sort the operations themselves before sorting them relative to each other. - val innerSortedList = commutativeAccumulation.combinedOperationsList.map { it.sort() } - addAllCombinedOperations(innerSortedList.sortedWith(COMPARABLE_OPERATION_COMPARATOR)) - }.build() - }.build() - NON_COMMUTATIVE_OPERATION -> toBuilder().apply { - nonCommutativeOperation = when (nonCommutativeOperation.operationTypeCase) { - OperationTypeCase.EXPONENTIATION -> nonCommutativeOperation.toBuilder().apply { - exponentiation = nonCommutativeOperation.exponentiation.toBuilder().apply { - leftOperand = nonCommutativeOperation.exponentiation.leftOperand.sort() - rightOperand = nonCommutativeOperation.exponentiation.rightOperand.sort() - }.build() - }.build() - OperationTypeCase.SQUARE_ROOT -> nonCommutativeOperation.toBuilder().apply { - squareRoot = nonCommutativeOperation.squareRoot.sort() - }.build() - OperationTypeCase.OPERATIONTYPE_NOT_SET, null -> nonCommutativeOperation - } - }.build() - CONSTANT_TERM, VARIABLE_TERM, COMPARISONTYPE_NOT_SET, null -> this - } - } } } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 523c3b9a6cc..62ee01b9175 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -3,15 +3,8 @@ package org.oppia.android.util.math import org.oppia.android.app.model.ComparableOperationList import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE import org.oppia.android.app.model.Real -import org.oppia.android.util.math.ExpressionToComparableOperationListConverter.Companion.toComparable +import org.oppia.android.util.math.ExpressionToComparableOperationListConverter.Companion.toComparableOperationList import org.oppia.android.util.math.ExpressionToLatexConverter.Companion.convertToLatex import org.oppia.android.util.math.NumericExpressionEvaluator.Companion.evaluate @@ -38,28 +31,4 @@ fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(div */ fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() -fun MathExpression.toComparableOperationList(): ComparableOperationList = - stripGroups().toComparable() - -private fun MathExpression.stripGroups(): MathExpression { - return when (expressionTypeCase) { - BINARY_OPERATION -> toBuilder().apply { - binaryOperation = binaryOperation.toBuilder().apply { - leftOperand = binaryOperation.leftOperand.stripGroups() - rightOperand = binaryOperation.rightOperand.stripGroups() - }.build() - }.build() - UNARY_OPERATION -> toBuilder().apply { - unaryOperation = unaryOperation.toBuilder().apply { - operand = unaryOperation.operand.stripGroups() - }.build() - }.build() - FUNCTION_CALL -> toBuilder().apply { - functionCall = functionCall.toBuilder().apply { - argument = functionCall.argument.stripGroups() - }.build() - }.build() - GROUP -> group.stripGroups() - CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> this - } -} +fun MathExpression.toComparableOperationList(): ComparableOperationList = toComparableOperationList() diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 0006df1e5d4..ef0e21a6bcd 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -9,6 +9,7 @@ import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET import kotlin.math.absoluteValue import kotlin.math.pow +// TODO: add tests. val REAL_COMPARATOR: Comparator by lazy { Comparator.comparing(Real::toDouble) } /** From f172bcf594c41a095f1110c8b8fd251ac83f5b88 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 18:43:10 -0800 Subject: [PATCH 075/134] Prepare for new tests. --- .../org/oppia/android/util/math/BUILD.bazel | 24 +- .../util/math/ComparatorExtensionsTest.kt | 20 + ...ToComparableOperationListConverterTest.kt} | 717 ++++++++++++++---- 3 files changed, 600 insertions(+), 161 deletions(-) create mode 100644 utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt rename utility/src/test/java/org/oppia/android/util/math/{ExpressionToComparableOperationListTest.kt => ExpressionToComparableOperationListConverterTest.kt} (66%) 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 index 1b49c30a3b1..40e676057f9 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -43,10 +43,26 @@ oppia_android_test( ) oppia_android_test( - name = "ExpressionToComparableOperationListTest", - srcs = ["ExpressionToComparableOperationListTest.kt"], + name = "ComparatorExtensionsTest", + srcs = ["ComparatorExtensionsTest.kt"], custom_package = "org.oppia.android.util.math", - test_class = "org.oppia.android.util.math.ExpressionToComparableOperationListTest", + test_class = "org.oppia.android.util.math.ComparatorExtensionsTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//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", + ], +) + +oppia_android_test( + name = "ExpressionToComparableOperationListConverterTest", + srcs = ["ExpressionToComparableOperationListConverterTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.ExpressionToComparableOperationListConverterTest", test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", @@ -57,7 +73,7 @@ oppia_android_test( "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", "//utility/src/main/java/org/oppia/android/util/math:extensions", - "//utility/src/main/java/org/oppia/android/util/math:parser", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) diff --git a/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt new file mode 100644 index 00000000000..bb27e759bb2 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt @@ -0,0 +1,20 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.LooperMode + +/** Tests for [Comparator] extensions. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class ComparatorExtensionsTest { + // TODO: finish tests + + @Test + fun test() { + throw Exception() + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverterTest.kt similarity index 66% rename from utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListTest.kt rename to utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverterTest.kt index d9599c0b32d..144a7ec273e 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverterTest.kt @@ -11,39 +11,204 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingM import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode -/** Tests for [MathExpressionParser]. */ +/** Tests for [ExpressionToComparableOperationListConverter]. */ // SameParameterValue: tests should have specific context included/excluded for readability. @Suppress("SameParameterValue") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) -class ExpressionToComparableOperationListTest { +class ExpressionToComparableOperationListConverterTest { // TODO: add high-level checks for the three types, but don't test in detail since there are // separate suites. Also, document the separate suites' existence in this suites's KDoc. - @Test - fun testToComparableOperation() { - // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). + // TODO: use utility directly + // TODO: finish tests. + // TODO: add tests for comparator/sorting & negation simplification? + + /* Operation creation */ + // testConvert_constantExpression_returnsConstantOperation + // testConvert_variableExpression_returnsVariableOperation + // testConvert_addition_returnsSummation + // testConvert_subtraction_returnsSummationOfNegative + // testConvert_multiplication_returnsProduct + // testConvert_division_returnsProductOfInverted + // testConvert_exponentiation_returnsNonCommutativeOperation + // testConvert_squareRoot_returnsNonCommutativeOperation + // testConvert_negatedVariable_returnsNegativeVariableOperation + // testConvert_positiveVariable_returnsVariableOperation + // testConvert_positiveOfNegativeVariable_returnsNegativeVariableOperation + // testConvert_subtractionOfNegative_returnsSummationWithPositives + // testConvert_multipleAdditions_returnsCombinedSummation + // testConvert_multipleSubtractions_returnsCombinedSummation + // testConvert_additionsAndSubtractions_returnsCombinedSummation + // testConvert_additionsWithNestedAdds_returnsCompletelyCombinedSummation + // testConvert_subtractsWithNestedSubs_returnsCompletelyCombinedSummation + // testConvert_additionsAndSubtractionsWithNested_returnsCombinedSummation + // testConvert_multipleMultiplications_returnsCombinedProduct + // testConvert_multipleDivisions_returnsCombinedProduct + // testConvert_multiplicationsAndDivisions_returnsCombinedProduct + // testConvert_multiplicationsWithNestedMults_returnsCompletelyCombinedProduct + // testConvert_divisionsWithNestedDivs_returnsCompletelyCombinedProduct + // testConvert_multiplicationsAndDivisionsWithNested_returnsCombinedProduct + // testConvert_multiplicationWithOneNegative_returnsNegativeProduct + // testConvert_multiplicationWithTwoNegatives_returnsPositiveProduct + // testConvert_multiplicationWithThreeNegatives_returnsNegativeProduct + // testConvert_combinedMultDivWithNested_evenNegatives_returnsPositiveProduct + // testConvert_combinedMultDivWithNested_oddNegatives_returnsNegativeProduct + // testConvert_additionAndExp_returnsSummationWithNonCommutative + // testConvert_additionAndSquareRoot_returnsSummationWithNonCommutative + // testConvert_additionWithinExp_returnsSummationWithinNonCommutative + // testConvert_additionWithinSquareRoot_returnsSummationWithinNonCommutative + // testConvert_multiplicationAndExp_returnsProductWithNonCommutative + // testConvert_multiplicationAndSquareRoot_returnsProductWithNonCommutative + // testConvert_multiplicationWithinExp_returnsProductWithinNonCommutative + // testConvert_multiplicationWithinSquareRoot_returnsProductWithinNonCommutative + // testConvert_additionAndMultiplication_returnsSummationOfProduct + // testConvert_multiplicationAndGroupedAddition_returnsProductOfSummation + + /* Top-level operation sorting */ + // testConvert_additionThenSquareRoot_samePrecedence_returnsOpWithSummationFirst + // testConvert_squareRootThenAddition_samePrecedence_returnsOpWithSummationFirst + // testConvert_additionThenExp_samePrecedence_returnsOpWithSummationFirst + // testConvert_exponentiationThenAddition_samePrecedence_returnsOpWithSummationFirst + // testConvert_constantThenSquareRoot_samePrecedence_returnsOpWithNonCommutativeFirst + // testConvert_squareRootThenConstant_samePrecedence_returnsOpWithNonCommutativeFirst + // testConvert_constantThenExp_samePrecedence_returnsOpWithNonCommutativeFirst + // testConvert_exponentiationThenConstant_samePrecedence_returnsOpWithNonCommutativeFirst + // testConvert_constantThenVariable_samePrecedence_returnsOpWithConstantFirst + // testConvert_variableThenConstant_samePrecedence_returnsOpWithConstantFirst + // testConvert_twoVariables_negatedThenInverted_returnsOpWithNegatedFirst + // testConvert_twoVariables_invertedThenNegated_returnsOpWithNegatedFirst + + /* Accumulator sorting */ + // TODO: mention no tiebreakers since there can't be summations or products adjacent with others + // of the same type. + // testConvert_additionAndMult_samePrecedence_returnsOpWithSummationFirst + // testConvert_multiplicationAndAddition_samePrecedence_returnsOpWithSummationFirst + // testConvert_additionAndMult_samePrecedenceAsNested_returnsOpWithSummationFirst + // testConvert_multiplicationAndAddition_samePrecedenceAsNested_returnsOpWithSummationFirst + + /* Non-commutative sorting */ + // testConvert_addExpThenSqrt_samePrecedence_returnsOpWithExpThenSqrt + // testConvert_addSqrtThenExp_samePrecedence_returnsOpWithExpThenSqrt + // testConvert_addTwoExps_lhs1Const_rhs1Const_lhs2Const_rhs2Const_returnsOpWithExp1Then2 + // ... parameterized: + // const^const const^const + // var^const const^const + // const^var const^const + // var^var const^const + // + // const^const var^const + // var^const var^const + // const^var var^const + // var^var var^const + // + // const^const const^var + // var^const const^var + // const^var const^var + // var^var const^var + // + // const^const var^var + // var^const var^var + // const^var var^var + // var^var var^var + // ... + // testConvert_addTwoSqrts_leftConst_rightConst_returnsOpWithSqrt1ThenSqrt2 + // testConvert_addTwoSqrts_leftVar_rightConst_returnsOpWithSqrt2ThenSqrt1 + // testConvert_addTwoSqrts_leftConst_rightVar_returnsOpWithSqrt1ThenSqrt2 + // testConvert_addTwoSqrts_leftVar_rightVar_returnsOpWithSqrt1ThenSqrt2 + + // testConvert_addTwoExps_lhs1Var_rhs1Const_lhs2Const_rhs2Const_returnsOpWithExp2Then1 + // testConvert_addTwoExps_lhs1Const_rhs1Var_lhs2Const_rhs2Const_returnsOpWithExp2Then1 + // testConvert_addTwoExps_lhs1Const_rhs1Var_lhs2Const_rhs2Var_returnsOpWithExp1Then2 + // testConvert_addTwoExps_lhs1Const_rhs1Var_lhs2Var_rhs2Const_returnsOpWithExp1Then2 + + /* Constant sorting */ + // testConvert_addTwoConstants_leftInteger2_rightInteger3_returnsOpWith2Then3 + // ... parameterized: + // left: 2 right: 3 + // left: 3 right: 2 + // + // left: 2 right: 3.14 + // left: 3.14 right: 2 + // + // left: 4 right: 3.14 + // left: 3.14 right: 4 + // + // left: 3.14 right: 6.28 + // left: 6.28 right: 3.14 + // ... + + /* Variable sorting */ + // testConvert_addTwoVariables_leftX_rightX_returnsOpBothXs + // testConvert_addTwoVariables_leftX_rightY_returnsOpWithXThenY + // ... parameterized: + // x, y; y, x; y, z; z, y; x, y, z; z, y, x + // ... + + /* Combined operations */ + // testConvert_allOperations_withNestedGroups_returnsCorrectlyStructuredAndOrderedOperation + + /* Equivalence checks */ + // testEquals_twoAdditionOps_differentByCommutativity_areEqual + // testEquals_twoAdditionOps_differentByAssociativity_areEqual + // testEquals_twoAdditionOps_differentByAssociativityAndCommutativity_areEqual + // testEquals_twoAdditionOps_differentByValue_areNotEqual + // testEquals_twoAdditionOps_differentByEvaluation_areNotEqual + // testEquals_twoMultOps_differentByCommutativity_areEqual + // testEquals_twoMultOps_differentByAssociativity_areEqual + // testEquals_twoMultOps_differentByAssociativityAndCommutativity_areEqual + // testEquals_twoMultOps_differentByValue_areNotEqual + // testEquals_twoMultOps_differentByEvaluation_areNotEqual + // TODO: for this & the next one, test with three operations (e.g. 2 / 3 / 4). + // testEquals_twoSubOps_same_areEqual + // testEquals_twoSubOps_differentByOrder_areNotEqual + // testEquals_twoSubOps_differentByValue_areNotEqual + // testEquals_twoDivOps_same_areEqual + // testEquals_twoDivOps_differentByOrder_areNotEqual + // testEquals_twoDivOps_differentByValue_areNotEqual + // testEquals_twoExps_same_areEqual + // testEquals_twoExps_differentByOrder_areNotEqual + // testEquals_twoExps_differentByValue_areNotEqual + // testEquals_twoSqrts_same_areEqual + // testEquals_twoSqrts_differentByValue_areNotEqual + // testEquals_twoOps_addsAndSubs_differentByOrder_areEqual + // testEquals_twoOps_multsAndDivs_differentByOrder_areEqual + // testEquals_twoOps_addsSubsMultsAndDivs_differentByOrder_areEqual + // testEquals_twoOps_allOperations_differentByOrder_areEqual + // testEquals_twoOps_allOperations_oneNestedDifferentByValue_areNotEqual + // testEquals_twoOps_allOperations_oneMissingTerm_areNotEqual - val exp1 = parseNumericExpressionSuccessfullyWithAllErrors("1") - assertThat(exp1.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test1() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } } + } - val exp2 = parseNumericExpressionSuccessfullyWithAllErrors("-1") - assertThat(exp2.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test2() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("-1") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isTrue() hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } } + } - val exp3 = parseNumericExpressionSuccessfullyWithAllErrors("1+3+4") - assertThat(exp3.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test3() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+3+4") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -71,9 +236,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp4 = parseNumericExpressionSuccessfullyWithAllErrors("-1-2-3") - assertThat(exp4.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test4() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("-1-2-3") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -101,9 +270,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp5 = parseNumericExpressionSuccessfullyWithAllErrors("1+2-3") - assertThat(exp5.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test5() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+2-3") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -131,9 +304,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp6 = parseNumericExpressionSuccessfullyWithAllErrors("2*3*4") - assertThat(exp6.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test6() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("2*3*4") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -161,9 +338,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp7 = parseNumericExpressionSuccessfullyWithAllErrors("1-2*3") - assertThat(exp7.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test7() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1-2*3") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -198,9 +379,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp8 = parseNumericExpressionSuccessfullyWithAllErrors("2*3-4") - assertThat(exp8.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test8() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("2*3-4") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -235,9 +420,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp9 = parseNumericExpressionSuccessfullyWithAllErrors("1+2*3-4+8*7*6-9") - assertThat(exp9.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test9() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+2*3-4+8*7*6-9") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -314,9 +503,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp10 = parseNumericExpressionSuccessfullyWithAllErrors("2/3/4") - assertThat(exp10.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test10() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("2/3/4") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -344,9 +537,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp11 = parseNumericExpressionSuccessfullyWithoutOptionalErrors("2^3^4") - assertThat(exp11.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test11() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithoutOptionalErrors("2^3^4") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() nonCommutativeOperation { @@ -383,9 +580,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp12 = parseNumericExpressionSuccessfullyWithAllErrors("1+2/3+3") - assertThat(exp12.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test12() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+2/3+3") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -427,9 +628,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp13 = parseNumericExpressionSuccessfullyWithAllErrors("1+(2/3)+3") - assertThat(exp13.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test13() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+(2/3)+3") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -471,9 +676,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp14 = parseNumericExpressionSuccessfullyWithAllErrors("1+2^3+3") - assertThat(exp14.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test14() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+2^3+3") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -516,9 +725,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp15 = parseNumericExpressionSuccessfullyWithAllErrors("1+(2^3)+3") - assertThat(exp15.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test15() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+(2^3)+3") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -561,9 +774,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp16 = parseNumericExpressionSuccessfullyWithAllErrors("2*3/4*7") - assertThat(exp16.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test16() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("2*3/4*7") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -598,9 +815,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp17 = parseNumericExpressionSuccessfullyWithAllErrors("2*(3/4)*7") - assertThat(exp17.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test17() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("2*(3/4)*7") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -635,9 +856,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp18 = parseNumericExpressionSuccessfullyWithAllErrors("-3*sqrt(2)") - assertThat(exp18.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test18() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("-3*sqrt(2)") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isTrue() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -664,9 +889,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp19 = parseNumericExpressionSuccessfullyWithAllErrors("1+(2+(3+(4+5)))") - assertThat(exp19.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test19() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+(2+(3+(4+5)))") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -708,9 +937,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp20 = parseNumericExpressionSuccessfullyWithAllErrors("2*(3*(4*(5*6)))") - assertThat(exp20.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test20() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("2*(3*(4*(5*6)))") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -752,27 +985,39 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp21 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") - assertThat(exp21.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test21() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("x") } } + } - val exp22 = parseAlgebraicExpressionSuccessfullyWithAllErrors("-x") - assertThat(exp22.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test22() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("-x") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isTrue() hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("x") } } + } - val exp23 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x+y") - assertThat(exp23.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test23() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x+y") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -800,9 +1045,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp24 = parseAlgebraicExpressionSuccessfullyWithAllErrors("-1-x-y") - assertThat(exp24.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test24() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("-1-x-y") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -830,9 +1079,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp25 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x-y") - assertThat(exp25.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test25() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x-y") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -860,9 +1113,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp26 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2xy") - assertThat(exp26.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test26() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2xy") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -890,9 +1147,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp27 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1-xy") - assertThat(exp27.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test27() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1-xy") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -927,9 +1188,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp28 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy-4") - assertThat(exp28.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test28() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy-4") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -964,9 +1229,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp29 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+xy-4+yz-9") - assertThat(exp29.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test29() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+xy-4+yz-9") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1036,9 +1305,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp30 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2/x/y") - assertThat(exp30.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test30() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2/x/y") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -1066,9 +1339,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp31 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("x^3^4") - assertThat(exp31.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test31() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("x^3^4") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() nonCommutativeOperation { @@ -1105,9 +1382,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp32 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x/y+z") - assertThat(exp32.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test32() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x/y+z") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1149,9 +1430,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp33 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x/y)+z") - assertThat(exp33.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test33() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x/y)+z") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1193,9 +1478,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp34 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x^3+y") - assertThat(exp34.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test34() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x^3+y") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1238,9 +1527,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp35 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x^3)+y") - assertThat(exp35.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test35() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x^3)+y") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1283,9 +1576,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp36 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*x/y*z") - assertThat(exp36.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test36() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*x/y*z") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -1320,9 +1617,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp37 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(x/y)*z") - assertThat(exp37.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test37() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(x/y)*z") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -1357,9 +1658,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp38 = parseAlgebraicExpressionSuccessfullyWithAllErrors("-2*sqrt(x)") - assertThat(exp38.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test38() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("-2*sqrt(x)") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isTrue() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -1386,9 +1691,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp39 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x+(3+(z+y)))") - assertThat(exp39.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test39() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x+(3+(z+y)))") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1430,9 +1739,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp40 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(x*(4*(zy)))") - assertThat(exp40.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test40() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(x*(4*(zy)))") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -1474,101 +1787,191 @@ class ExpressionToComparableOperationListTest { } } } + } - // Equality tests: - val list1 = createComparableOperationListFromNumericExpression("(1+2)+3") - val list2 = createComparableOperationListFromNumericExpression("1+(2+3)") - assertThat(list1).isEqualTo(list2) - - val list3 = createComparableOperationListFromNumericExpression("1+2+3") - val list4 = createComparableOperationListFromNumericExpression("3+2+1") - assertThat(list3).isEqualTo(list4) + // TODO: Equality tests: + @Test + fun test41() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("(1+2)+3") + val secondList = createComparableOperationListFromNumericExpression("1+(2+3)") + assertThat(firstList).isEqualTo(secondList) + } - val list5 = createComparableOperationListFromNumericExpression("1-2-3") - val list6 = createComparableOperationListFromNumericExpression("-3 + -2 + 1") - assertThat(list5).isEqualTo(list6) + @Test + fun test42() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("1+2+3") + val secondList = createComparableOperationListFromNumericExpression("3+2+1") + assertThat(firstList).isEqualTo(secondList) + } - val list7 = createComparableOperationListFromNumericExpression("1-2-3") - val list8 = createComparableOperationListFromNumericExpression("-3-2+1") - assertThat(list7).isEqualTo(list8) + @Test + fun test43() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("1-2-3") + val secondList = createComparableOperationListFromNumericExpression("-3 + -2 + 1") + assertThat(firstList).isEqualTo(secondList) + } - val list9 = createComparableOperationListFromNumericExpression("1-2-3") - val list10 = createComparableOperationListFromNumericExpression("-3-2+1") - assertThat(list9).isEqualTo(list10) + @Test + fun test44() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("1-2-3") + val secondList = createComparableOperationListFromNumericExpression("-3-2+1") + assertThat(firstList).isEqualTo(secondList) + } - val list11 = createComparableOperationListFromNumericExpression("1-2-3") - val list12 = createComparableOperationListFromNumericExpression("3-2-1") - assertThat(list11).isNotEqualTo(list12) + @Test + fun test45() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("1-2-3") + val secondList = createComparableOperationListFromNumericExpression("-3-2+1") + assertThat(firstList).isEqualTo(secondList) + } - val list13 = createComparableOperationListFromNumericExpression("2*3*4") - val list14 = createComparableOperationListFromNumericExpression("4*3*2") - assertThat(list13).isEqualTo(list14) + @Test + fun test46() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("1-2-3") + val secondList = createComparableOperationListFromNumericExpression("3-2-1") + assertThat(firstList).isNotEqualTo(secondList) + } - val list15 = createComparableOperationListFromNumericExpression("2*(3/4)") - val list16 = createComparableOperationListFromNumericExpression("3/4*2") - assertThat(list15).isEqualTo(list16) + @Test + fun test47() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("2*3*4") + val secondList = createComparableOperationListFromNumericExpression("4*3*2") + assertThat(firstList).isEqualTo(secondList) + } - val list17 = createComparableOperationListFromNumericExpression("2*3/4") - val list18 = createComparableOperationListFromNumericExpression("3/4*2") - assertThat(list17).isEqualTo(list18) + @Test + fun test48() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("2*(3/4)") + val secondList = createComparableOperationListFromNumericExpression("3/4*2") + assertThat(firstList).isEqualTo(secondList) + } - val list45 = createComparableOperationListFromNumericExpression("2*3/4") - val list46 = createComparableOperationListFromNumericExpression("2*3*4") - assertThat(list45).isNotEqualTo(list46) + @Test + fun test49() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("2*3/4") + val secondList = createComparableOperationListFromNumericExpression("3/4*2") + assertThat(firstList).isEqualTo(secondList) + } - val list19 = createComparableOperationListFromNumericExpression("2*3/4") - val list20 = createComparableOperationListFromNumericExpression("2*4/3") - assertThat(list19).isNotEqualTo(list20) + @Test + fun test50() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("2*3/4") + val secondList = createComparableOperationListFromNumericExpression("2*3*4") + assertThat(firstList).isNotEqualTo(secondList) + } - val list21 = createComparableOperationListFromNumericExpression("2*3/4*7") - val list22 = createComparableOperationListFromNumericExpression("3/4*7*2") - assertThat(list21).isEqualTo(list22) + @Test + fun test51() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("2*3/4") + val secondList = createComparableOperationListFromNumericExpression("2*4/3") + assertThat(firstList).isNotEqualTo(secondList) + } - val list23 = createComparableOperationListFromNumericExpression("2*3/4*7") - val list24 = createComparableOperationListFromNumericExpression("7*(3*2/4)") - assertThat(list23).isEqualTo(list24) + @Test + fun test52() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("2*3/4*7") + val secondList = createComparableOperationListFromNumericExpression("3/4*7*2") + assertThat(firstList).isEqualTo(secondList) + } - val list25 = createComparableOperationListFromNumericExpression("2*3/4*7") - val list26 = createComparableOperationListFromNumericExpression("7*3*2/4") - assertThat(list25).isEqualTo(list26) + @Test + fun test53() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("2*3/4*7") + val secondList = createComparableOperationListFromNumericExpression("7*(3*2/4)") + assertThat(firstList).isEqualTo(secondList) + } - val list27 = createComparableOperationListFromNumericExpression("-2*3") - val list28 = createComparableOperationListFromNumericExpression("3*-2") - assertThat(list27).isEqualTo(list28) + @Test + fun test54() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("2*3/4*7") + val secondList = createComparableOperationListFromNumericExpression("7*3*2/4") + assertThat(firstList).isEqualTo(secondList) + } - val list29 = createComparableOperationListFromNumericExpression("2^3") - val list30 = createComparableOperationListFromNumericExpression("3^2") - assertThat(list29).isNotEqualTo(list30) + @Test + fun test55() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("-2*3") + val secondList = createComparableOperationListFromNumericExpression("3*-2") + assertThat(firstList).isEqualTo(secondList) + } - val list31 = createComparableOperationListFromNumericExpression("-(1+2)") - val list32 = createComparableOperationListFromNumericExpression("-1+2") - assertThat(list31).isNotEqualTo(list32) + @Test + fun test56() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("2^3") + val secondList = createComparableOperationListFromNumericExpression("3^2") + assertThat(firstList).isNotEqualTo(secondList) + } - val list33 = createComparableOperationListFromNumericExpression("-(1+2)") - val list34 = createComparableOperationListFromNumericExpression("-1-2") - assertThat(list33).isNotEqualTo(list34) + @Test + fun test57() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("-(1+2)") + val secondList = createComparableOperationListFromNumericExpression("-1+2") + assertThat(firstList).isNotEqualTo(secondList) + } - val list35 = createComparableOperationListFromAlgebraicExpression("x(x+1)") - val list36 = createComparableOperationListFromAlgebraicExpression("(1+x)x") - assertThat(list35).isEqualTo(list36) + @Test + fun test58() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("-(1+2)") + val secondList = createComparableOperationListFromNumericExpression("-1-2") + assertThat(firstList).isNotEqualTo(secondList) + } - val list37 = createComparableOperationListFromAlgebraicExpression("x(x+1)") - val list38 = createComparableOperationListFromAlgebraicExpression("x^2+x") - assertThat(list37).isNotEqualTo(list38) + @Test + fun test59() { + // TODO: do something with this + val firstList = createComparableOperationListFromAlgebraicExpression("x(x+1)") + val secondList = createComparableOperationListFromAlgebraicExpression("(1+x)x") + assertThat(firstList).isEqualTo(secondList) + } - val list39 = createComparableOperationListFromAlgebraicExpression("x^2*sqrt(x)") - val list40 = createComparableOperationListFromAlgebraicExpression("x") - assertThat(list39).isNotEqualTo(list40) + @Test + fun test60() { + // TODO: do something with this + val firstList = createComparableOperationListFromAlgebraicExpression("x(x+1)") + val secondList = createComparableOperationListFromAlgebraicExpression("x^2+x") + assertThat(firstList).isNotEqualTo(secondList) + } - val list41 = createComparableOperationListFromAlgebraicExpression("xyz") - val list42 = createComparableOperationListFromAlgebraicExpression("zyx") - assertThat(list41).isEqualTo(list42) + @Test + fun test61() { + // TODO: do something with this + val firstList = createComparableOperationListFromAlgebraicExpression("x^2*sqrt(x)") + val secondList = createComparableOperationListFromAlgebraicExpression("x") + assertThat(firstList).isNotEqualTo(secondList) + } - val list43 = createComparableOperationListFromAlgebraicExpression("1+xy-2") - val list44 = createComparableOperationListFromAlgebraicExpression("-2+1+yx") - assertThat(list43).isEqualTo(list44) + @Test + fun test62() { + // TODO: do something with this + val firstList = createComparableOperationListFromAlgebraicExpression("xyz") + val secondList = createComparableOperationListFromAlgebraicExpression("zyx") + assertThat(firstList).isEqualTo(secondList) + } - // TODO: add tests for comparator/sorting & negation simplification? + @Test + fun test63() { + // TODO: do something with this + val firstList = createComparableOperationListFromAlgebraicExpression("1+xy-2") + val secondList = createComparableOperationListFromAlgebraicExpression("-2+1+yx") + assertThat(firstList).isEqualTo(secondList) } private fun createComparableOperationListFromNumericExpression(expression: String) = From 65a9fe1e65e8e97deaadbc9213f632ee6c4af683 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 18:53:39 -0800 Subject: [PATCH 076/134] Remove the ComparableOperationList wrapper. --- model/src/main/proto/math.proto | 56 +++++++++---------- scripts/assets/test_file_exemptions.textproto | 2 +- .../oppia/android/testing/math/BUILD.bazel | 4 +- ...bject.kt => ComparableOperationSubject.kt} | 56 +++++++++---------- 4 files changed, 55 insertions(+), 63 deletions(-) rename testing/src/main/java/org/oppia/android/testing/math/{ComparableOperationListSubject.kt => ComparableOperationSubject.kt} (87%) diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index d1fc9f36419..4e3989ec3e3 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -189,7 +189,8 @@ message MathEquation { MathExpression right_side = 2; } -// Represents a list of comparable mathematics operations. +// An operation that can be compared in a way that does not change the value based on commutativity +// or associativity. // // 'Comparable' here means that this structure provides a trivial way to compare commutative and // associative operations (i.e. by extracting terms from multiple subsequent commutative & @@ -199,33 +200,29 @@ message MathEquation { // using standard proto equals checking). Also note that care must be taken when performing equality // checking since this structure can contain floating point values that require an epsilon check to // approximate equality. -message ComparableOperationList { - // An operation that can be compared in a way that does not change the value based on - // commutativity or associativity. - message ComparableOperation { - // Indicates that this operation (e.g. x) should be treated as negated (e.g. -x). - bool is_negated = 1; - - // Indicates that this operation (e.g. x) should be treated as a multiplicative inverse - // (e.g. 1/x). - bool is_inverted = 2; - - // The supported comparison types. - oneof comparison_type { - // Indicates that this operation is a commutative accumulation (that is, a list of subsequent - // operations of the same type that are commutative, e.g. addition or multiplication). - CommutativeAccumulation commutative_accumulation = 3; - - // Indicates that this operation is a non-commutative operation and thus cannot be - // accumulated (e.g. exponentiation). - NonCommutativeOperation non_commutative_operation = 4; - - // Indicates that this operation represents a constant value. - Real constant_term = 5; - - // Indicates that this operation represents a variable. - string variable_term = 6; - } +message ComparableOperation { + // Indicates that this operation (e.g. x) should be treated as negated (e.g. -x). + bool is_negated = 1; + + // Indicates that this operation (e.g. x) should be treated as a multiplicative inverse + // (e.g. 1/x). + bool is_inverted = 2; + + // The supported comparison types. + oneof comparison_type { + // Indicates that this operation is a commutative accumulation (that is, a list of subsequent + // operations of the same type that are commutative, e.g. addition or multiplication). + CommutativeAccumulation commutative_accumulation = 3; + + // Indicates that this operation is a non-commutative operation and thus cannot be + // accumulated (e.g. exponentiation). + NonCommutativeOperation non_commutative_operation = 4; + + // Indicates that this operation represents a constant value. + Real constant_term = 5; + + // Indicates that this operation represents a variable. + string variable_term = 6; } // Represents an accumulation of operations (such as a summation or product). @@ -285,7 +282,4 @@ message ComparableOperationList { ComparableOperation right_operand = 2; } } - - // The root of the operation list (i.e. the lowest precedent operation of the expression). - ComparableOperation root_operation = 1; } diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index b0578bf3b80..b2f2a5f5a99 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -646,7 +646,7 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/Ge exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/ImageViewMatcher.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/KonfettiViewMatcher.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/DefineAppLanguageLocaleContext.kt" -exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/ComparableOperationSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt" diff --git a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel index 98eb5f9cdd1..0e6b8bf7989 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel @@ -7,10 +7,10 @@ load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") # TODO(#2747): Move these libraries to be under utility/.../math/testing. kt_android_library( - name = "comparable_operation_list_subject", + name = "comparable_operation_subject", testonly = True, srcs = [ - "ComparableOperationListSubject.kt", + "ComparableOperationSubject.kt", ], visibility = [ "//:oppia_testing_visibility", diff --git a/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationSubject.kt similarity index 87% rename from testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt rename to testing/src/main/java/org/oppia/android/testing/math/ComparableOperationSubject.kt index 3d71f819677..06dd8bae7ba 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationSubject.kt @@ -7,16 +7,15 @@ import com.google.common.truth.StringSubject import com.google.common.truth.Truth.assertAbout import com.google.common.truth.Truth.assertThat import com.google.common.truth.extensions.proto.LiteProtoSubject -import org.oppia.android.app.model.ComparableOperationList -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase +import org.oppia.android.app.model.ComparableOperation +import org.oppia.android.app.model.ComparableOperation.ComparisonTypeCase import org.oppia.android.app.model.Real import org.oppia.android.testing.math.RealSubject.Companion.assertThat // TODO(#4098): Add tests for this class. /** - * Truth subject for verifying properties of [ComparableOperationList]s. + * Truth subject for verifying properties of [ComparableOperation]s. * * This subject makes use of a custom Kotlin DSL to test the structure of a comparable operation * list. This structure allows for recursive verification of the structure since the structure @@ -25,7 +24,7 @@ import org.oppia.android.testing.math.RealSubject.Companion.assertThat * comparators for all syntactical options): * * ```kotlin - * assertThat(comparableOperationList).hasStructureThatMatches { + * assertThat(ComparableOperation).hasStructureThatMatches { * hasNegatedPropertyThat().isFalse() * hasInvertedPropertyThat().isFalse() * commutativeAccumulationWithType(SUMMATION) { @@ -58,22 +57,21 @@ import org.oppia.android.testing.math.RealSubject.Companion.assertThat * The above verifies the following structure corresponding to the expression 1+3+4. * * Note that this class is also a [LiteProtoSubject] so other aspects of the underlying - * [ComparableOperationList] proto can be verified through inherited methods. + * [ComparableOperation] proto can be verified through inherited methods. * * Call [assertThat] to create the subject. */ -class ComparableOperationListSubject private constructor( +class ComparableOperationSubject private constructor( metadata: FailureMetadata, - private val actual: ComparableOperationList + private val actual: ComparableOperation ) : LiteProtoSubject(metadata, actual) { /** - * Begins the structure syntax matcher for the root of the [ComparableOperationList] corresponding - * to this subject (per [ComparableOperationList.getRootOperation]). + * Begins the structure syntax matcher for [ComparableOperation] being tested as the subject. * * See [ComparableOperationComparator] for syntax. */ fun hasStructureThatMatches(init: ComparableOperationComparator.() -> Unit) { - ComparableOperationComparator.createFrom(actual.rootOperation).also(init) + ComparableOperationComparator.createFrom(actual).also(init) } /** @@ -124,7 +122,7 @@ class ComparableOperationListSubject private constructor( * specified type. See [CommutativeAccumulationComparator] for example syntax. */ fun commutativeAccumulationWithType( - type: ComparableOperationList.CommutativeAccumulation.AccumulationType, + type: ComparableOperation.CommutativeAccumulation.AccumulationType, init: CommutativeAccumulationComparator.() -> Unit ) { CommutativeAccumulationComparator.createFrom(type, operation).also(init) @@ -199,11 +197,11 @@ class ComparableOperationListSubject private constructor( */ @ComparableOperationComparatorMarker class CommutativeAccumulationComparator private constructor( - private val accumulation: ComparableOperationList.CommutativeAccumulation + private val accumulation: ComparableOperation.CommutativeAccumulation ) { /** * Returns a [IntegerSubject] to test - * [ComparableOperationList.CommutativeAccumulation.getCombinedOperationsCount]. + * [ComparableOperation.CommutativeAccumulation.getCombinedOperationsCount]. * * This method never fails since the underlying property defaults to 0 if there are no * operations in the accumulation. @@ -234,7 +232,7 @@ class ComparableOperationListSubject private constructor( * specified type. */ fun createFrom( - type: ComparableOperationList.CommutativeAccumulation.AccumulationType, + type: ComparableOperation.CommutativeAccumulation.AccumulationType, operation: ComparableOperation ): CommutativeAccumulationComparator { assertThat(operation.comparisonTypeCase) @@ -259,11 +257,11 @@ class ComparableOperationListSubject private constructor( */ @ComparableOperationComparatorMarker class NonCommutativeOperationComparator private constructor( - private val operation: ComparableOperationList.NonCommutativeOperation + private val operation: ComparableOperation.NonCommutativeOperation ) { /** * Begins structure matching for this operation as an exponentiation per - * [ComparableOperationList.NonCommutativeOperation.getExponentiation]. + * [ComparableOperation.NonCommutativeOperation.getExponentiation]. * * This method will fail if the operation corresponding to the subject is not an exponentiation. * See [BinaryOperationComparator] for specifics on the operation comparator used here. Example @@ -277,14 +275,14 @@ class ComparableOperationListSubject private constructor( */ fun exponentiation(init: BinaryOperationComparator.() -> Unit) { verifyTypeAs( - ComparableOperationList.NonCommutativeOperation.OperationTypeCase.EXPONENTIATION + ComparableOperation.NonCommutativeOperation.OperationTypeCase.EXPONENTIATION ) BinaryOperationComparator.createFrom(operation.exponentiation).also(init) } /** * Begins structure matching for this operation as a square root operation per - * [ComparableOperationList.NonCommutativeOperation.getSquareRoot]. + * [ComparableOperation.NonCommutativeOperation.getSquareRoot]. * * This method will fail if the operation corresponding to the subject is not a square root. The * argument is another [ComparableOperation] hence the utilization of @@ -299,12 +297,12 @@ class ComparableOperationListSubject private constructor( fun squareRootWithArgument( init: ComparableOperationComparator.() -> Unit ) { - verifyTypeAs(ComparableOperationList.NonCommutativeOperation.OperationTypeCase.SQUARE_ROOT) + verifyTypeAs(ComparableOperation.NonCommutativeOperation.OperationTypeCase.SQUARE_ROOT) ComparableOperationComparator.createFrom(operation.squareRoot).also(init) } private fun verifyTypeAs( - type: ComparableOperationList.NonCommutativeOperation.OperationTypeCase + type: ComparableOperation.NonCommutativeOperation.OperationTypeCase ) { assertThat(operation.operationTypeCase).isEqualTo(type) } @@ -344,11 +342,11 @@ class ComparableOperationListSubject private constructor( */ @ComparableOperationComparatorMarker class BinaryOperationComparator private constructor( - private val operation: ComparableOperationList.NonCommutativeOperation.BinaryOperation + private val operation: ComparableOperation.NonCommutativeOperation.BinaryOperation ) { /** * Begins structure matching this operation's left operand per - * [ComparableOperationList.NonCommutativeOperation.BinaryOperation.getLeftOperand] for the + * [ComparableOperation.NonCommutativeOperation.BinaryOperation.getLeftOperand] for the * operation represented by this comparator. * * This method provides an [ComparableOperationComparator] to use to verify the constituent @@ -362,7 +360,7 @@ class ComparableOperationListSubject private constructor( /** * Begins structure matching this operation's right operand per - * [ComparableOperationList.NonCommutativeOperation.BinaryOperation.getRightOperand] for the + * [ComparableOperation.NonCommutativeOperation.BinaryOperation.getRightOperand] for the * operation represented by this comparator. * * This method provides an [ComparableOperationComparator] to use to verify the constituent @@ -380,7 +378,7 @@ class ComparableOperationListSubject private constructor( * binary operation. */ fun createFrom( - operation: ComparableOperationList.NonCommutativeOperation.BinaryOperation + operation: ComparableOperation.NonCommutativeOperation.BinaryOperation ): BinaryOperationComparator = BinaryOperationComparator(operation) } } @@ -458,10 +456,10 @@ class ComparableOperationListSubject private constructor( @DslMarker private annotation class ComparableOperationComparatorMarker /** - * Returns a new [ComparableOperationListSubject] to verify aspects of the specified - * [ComparableOperationList] value. + * Returns a new [ComparableOperationSubject] to verify aspects of the specified + * [ComparableOperation] value. */ - fun assertThat(actual: ComparableOperationList): ComparableOperationListSubject = - assertAbout(::ComparableOperationListSubject).that(actual) + fun assertThat(actual: ComparableOperation): ComparableOperationSubject = + assertAbout(::ComparableOperationSubject).that(actual) } } From fbd935cc170254f965a712d407403b14220367c2 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 19:06:13 -0800 Subject: [PATCH 077/134] Change parameterized method delimiter. --- .../oppia/android/testing/junit/OppiaParameterizedTestRunner.kt | 2 +- .../oppia/android/testing/junit/ParameterizedRunnerDelegate.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt index e237553a9fc..218dc1258aa 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt @@ -56,7 +56,7 @@ import java.lang.reflect.Method * e.g.: * * ```bash - * bazel run //...:ExampleParameterizedTest --test_filter=testParams_multipleVals_isConsistent-first + * bazel run //...:ExampleParameterizedTest --test_filter=testParams_multipleVals_isConsistent_first * ``` * * Or, all of the iterations for that test can be run: diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt index dc9699b31d5..3e32dba92d6 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt @@ -43,7 +43,7 @@ class ParameterizedRunnerDelegate( override fun testName(method: FrameworkMethod?): String { return if (methodName != null) { - "${fetchTestNameFromParent(method)}-$iterationName" + "${fetchTestNameFromParent(method)}_$iterationName" } else fetchTestNameFromParent(method) } From 8420c563a99a859641d43972bdca60a1727a7390 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 19:18:46 -0800 Subject: [PATCH 078/134] Use utility directly in test. --- .../org/oppia/android/util/math/BUILD.bazel | 3 +- .../math/ExpressionToLatexConverterTest.kt | 90 +++++++++++++------ 2 files changed, 64 insertions(+), 29 deletions(-) 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 index 72b42b3832c..f2b8f412afc 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -50,14 +50,13 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", - "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", - "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_extensions_truth-liteproto-extension", "//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", "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt index 1af737e0583..d9125d519a5 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt @@ -6,8 +6,6 @@ import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression -import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat -import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode @@ -23,151 +21,189 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_number_returnsConstantLatex() { val exp = parseNumericExpressionWithAllErrors("1") - assertThat(exp).convertsToLatexStringThat().isEqualTo("1") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("1") } @Test fun testConvert_numericExp_unaryPlus_withoutOptionalErrors_returnLatexWithUnaryPlus() { val exp = parseNumericExpressionWithoutOptionalErrors("+1") - assertThat(exp).convertsToLatexStringThat().isEqualTo("+1") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("+1") } @Test fun testConvert_numericExp_unaryMinus_returnLatexWithUnaryMinus() { val exp = parseNumericExpressionWithAllErrors("-1") - assertThat(exp).convertsToLatexStringThat().isEqualTo("-1") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("-1") } @Test fun testConvert_numericExp_addition_returnsLatexWithAddition() { val exp = parseNumericExpressionWithAllErrors("1+2") - assertThat(exp).convertsToLatexStringThat().isEqualTo("1 + 2") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("1 + 2") } @Test fun testConvert_numericExp_subtraction_returnsLatexWithSubtract() { val exp = parseNumericExpressionWithAllErrors("1-2") - assertThat(exp).convertsToLatexStringThat().isEqualTo("1 - 2") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("1 - 2") } @Test fun testConvert_numericExp_multiplication_returnsLatexWithMultiplication() { val exp = parseNumericExpressionWithAllErrors("2*3") - assertThat(exp).convertsToLatexStringThat().isEqualTo("2 \\times 3") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("2 \\times 3") } @Test fun testConvert_numericExp_division_returnsLatexWithDivision() { val exp = parseNumericExpressionWithAllErrors("2/3") - assertThat(exp).convertsToLatexStringThat().isEqualTo("2 \\div 3") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("2 \\div 3") } @Test fun testConvert_numericExp_division_divAsFraction_returnsLatexWithFraction() { val exp = parseNumericExpressionWithAllErrors("2/3") - assertThat(exp).convertsWithFractionsToLatexStringThat().isEqualTo("\\frac{2}{3}") + val latex = exp.toRawLatex(divAsFraction = true) + + assertThat(latex).isEqualTo("\\frac{2}{3}") } @Test fun testConvert_numericExp_multipleDivisions_divAsFraction_returnsLatexWithFractions() { val exp = parseNumericExpressionWithAllErrors("2/3/4") - assertThat(exp).convertsWithFractionsToLatexStringThat().isEqualTo("\\frac{\\frac{2}{3}}{4}") + val latex = exp.toRawLatex(divAsFraction = true) + + assertThat(latex).isEqualTo("\\frac{\\frac{2}{3}}{4}") } @Test fun testConvert_numericExp_exponent_returnsLatexWithExponent() { val exp = parseNumericExpressionWithAllErrors("2^3") - assertThat(exp).convertsToLatexStringThat().isEqualTo("2 ^ {3}") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("2 ^ {3}") } @Test fun testConvert_numericExp_inlineSquareRoot_returnsLatexWithSquareRoot() { val exp = parseNumericExpressionWithAllErrors("√2") - assertThat(exp).convertsToLatexStringThat().isEqualTo("\\sqrt{2}") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("\\sqrt{2}") } @Test fun testConvert_numericExp_inlineSquareRoot_operationArg_returnsLatexWithSquareRoot() { val exp = parseNumericExpressionWithAllErrors("√(1+2)") - assertThat(exp).convertsToLatexStringThat().isEqualTo("\\sqrt{(1 + 2)}") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("\\sqrt{(1 + 2)}") } @Test fun testConvert_numericExp_squareRoot_returnsLatexWithSquareRoot() { val exp = parseNumericExpressionWithAllErrors("sqrt(2)") - assertThat(exp).convertsToLatexStringThat().isEqualTo("\\sqrt{2}") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("\\sqrt{2}") } @Test fun testConvert_numericExp_squareRoot_operationArg_returnsLatexWithSquareRoot() { val exp = parseNumericExpressionWithAllErrors("sqrt(1+2)") - assertThat(exp).convertsToLatexStringThat().isEqualTo("\\sqrt{1 + 2}") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("\\sqrt{1 + 2}") } @Test fun testConvert_numericExp_parentheses_returnsLatexWithGroup() { val exp = parseNumericExpressionWithAllErrors("2/(3+4)") - assertThat(exp).convertsToLatexStringThat().isEqualTo("2 \\div (3 + 4)") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("2 \\div (3 + 4)") } @Test fun testConvert_numericExp_exponentToGroup_returnsCorrectlyWrappedLatex() { val exp = parseNumericExpressionWithAllErrors("2^(7-3)") - assertThat(exp).convertsToLatexStringThat().isEqualTo("2 ^ {(7 - 3)}") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("2 ^ {(7 - 3)}") } @Test fun testConvert_algebraicExp_variable_returnsVariableLatex() { val exp = parseAlgebraicExpressionWithAllErrors("x") - assertThat(exp).convertsToLatexStringThat().isEqualTo("x") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("x") } @Test fun testConvert_algebraicExp_twoX_returnsLatexWithImplicitMultiplication() { val exp = parseAlgebraicExpressionWithAllErrors("2x") - assertThat(exp).convertsToLatexStringThat().isEqualTo("2x") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("2x") } @Test fun testConvert_algebraicEq_xEqualsOne_returnsLatexWithEquals() { val exp = parseAlgebraicEquationWithAllErrors("x=1") - assertThat(exp).convertsToLatexStringThat().isEqualTo("x = 1") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("x = 1") } @Test fun testConvert_algebraicEq_complexExpression_returnsCorrectLatex() { val exp = parseAlgebraicEquationWithAllErrors("(x+1)(x-2)=(x^3+2x^2-5x-6)/(x+3)") - assertThat(exp) - .convertsToLatexStringThat() - .isEqualTo("(x + 1)(x - 2) = (x ^ {3} + 2x ^ {2} - 5x - 6) \\div (x + 3)") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("(x + 1)(x - 2) = (x ^ {3} + 2x ^ {2} - 5x - 6) \\div (x + 3)") } @Test fun testConvert_algebraicEq_complexExpression_divAsFraction_returnsCorrectLatex() { val exp = parseAlgebraicEquationWithAllErrors("(x+1)(x-2)=(x^3+2x^2-5x-6)/(x+3)") - assertThat(exp) - .convertsWithFractionsToLatexStringThat() - .isEqualTo("(x + 1)(x - 2) = \\frac{(x ^ {3} + 2x ^ {2} - 5x - 6)}{(x + 3)}") + val latex = exp.toRawLatex(divAsFraction = true) + + assertThat(latex).isEqualTo("(x + 1)(x - 2) = \\frac{(x ^ {3} + 2x ^ {2} - 5x - 6)}{(x + 3)}") } private companion object { From b8a3e2e515aa7bc3316d7ff3a187f132cd81c3cb Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 19:27:04 -0800 Subject: [PATCH 079/134] Post-merge fixes. This adjusts for the removal of ComparableOperationList (i.e. no wrapper proto). --- .../org/oppia/android/util/math/BUILD.bazel | 6 ++-- ...pressionToComparableOperationConverter.kt} | 29 +++++++------------ .../util/math/MathExpressionExtensions.kt | 6 ++-- .../org/oppia/android/util/math/BUILD.bazel | 6 ++-- ...sionToComparableOperationConverterTest.kt} | 4 +-- 5 files changed, 22 insertions(+), 29 deletions(-) rename utility/src/main/java/org/oppia/android/util/math/{ExpressionToComparableOperationListConverter.kt => ExpressionToComparableOperationConverter.kt} (89%) rename utility/src/test/java/org/oppia/android/util/math/{ExpressionToComparableOperationListConverterTest.kt => ExpressionToComparableOperationConverterTest.kt} (99%) 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 index 05542af8731..d74e1683647 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -119,7 +119,7 @@ kt_android_library( "MathExpressionExtensions.kt", ], deps = [ - ":expression_to_comparable_operation_list_converter", + ":expression_to_comparable_operation_converter", ":expression_to_latex_converter", ":numeric_expression_evaluator", "//model/src/main/proto:math_java_proto_lite", @@ -161,9 +161,9 @@ kt_android_library( ) kt_android_library( - name = "expression_to_comparable_operation_list_converter", + name = "expression_to_comparable_operation_converter", srcs = [ - "ExpressionToComparableOperationListConverter.kt", + "ExpressionToComparableOperationConverter.kt", ], deps = [ ":comparator_extensions", diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt similarity index 89% rename from utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt rename to utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt index ef1008cab54..1dd214a78a2 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt @@ -1,15 +1,14 @@ package org.oppia.android.util.math -import org.oppia.android.app.model.ComparableOperationList -import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation -import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.PRODUCT -import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.SUMMATION -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.CONSTANT_TERM -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.NON_COMMUTATIVE_OPERATION -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.VARIABLE_TERM -import org.oppia.android.app.model.ComparableOperationList.NonCommutativeOperation -import org.oppia.android.app.model.ComparableOperationList.NonCommutativeOperation.OperationTypeCase +import org.oppia.android.app.model.ComparableOperation +import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation +import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.PRODUCT +import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.SUMMATION +import org.oppia.android.app.model.ComparableOperation.ComparisonTypeCase.CONSTANT_TERM +import org.oppia.android.app.model.ComparableOperation.ComparisonTypeCase.NON_COMMUTATIVE_OPERATION +import org.oppia.android.app.model.ComparableOperation.ComparisonTypeCase.VARIABLE_TERM +import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation +import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation.OperationTypeCase import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE @@ -30,7 +29,7 @@ import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE -class ExpressionToComparableOperationListConverter private constructor() { +class ExpressionToComparableOperationConverter private constructor() { companion object { // TODO: consider eliminating the comparator extensions. Probably should verify full test suite // & the old tests before deleting the old tests. @@ -87,13 +86,7 @@ class ExpressionToComparableOperationListConverter private constructor() { ) } - fun MathExpression.toComparableOperationList(): ComparableOperationList { - return ComparableOperationList.newBuilder().apply { - rootOperation = toComparableOperation() - }.build() - } - - private fun MathExpression.toComparableOperation(): ComparableOperation { + fun MathExpression.toComparableOperation(): ComparableOperation { return when (expressionTypeCase) { CONSTANT -> ComparableOperation.newBuilder().apply { constantTerm = constant diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 62ee01b9175..59268fca145 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -1,10 +1,10 @@ package org.oppia.android.util.math -import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.ComparableOperation import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression import org.oppia.android.app.model.Real -import org.oppia.android.util.math.ExpressionToComparableOperationListConverter.Companion.toComparableOperationList +import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.toComparableOperation import org.oppia.android.util.math.ExpressionToLatexConverter.Companion.convertToLatex import org.oppia.android.util.math.NumericExpressionEvaluator.Companion.evaluate @@ -31,4 +31,4 @@ fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(div */ fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() -fun MathExpression.toComparableOperationList(): ComparableOperationList = toComparableOperationList() +fun MathExpression.toComparableOperationList(): ComparableOperation = toComparableOperation() 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 index ca47eb44deb..dbf95666418 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -59,10 +59,10 @@ oppia_android_test( ) oppia_android_test( - name = "ExpressionToComparableOperationListConverterTest", - srcs = ["ExpressionToComparableOperationListConverterTest.kt"], + name = "ExpressionToComparableOperationConverterTest", + srcs = ["ExpressionToComparableOperationConverterTest.kt"], custom_package = "org.oppia.android.util.math", - test_class = "org.oppia.android.util.math.ExpressionToComparableOperationListConverterTest", + test_class = "org.oppia.android.util.math.ExpressionToComparableOperationConverterTest", test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt similarity index 99% rename from utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverterTest.kt rename to utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt index 144a7ec273e..fd2607b1702 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverterTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt @@ -11,12 +11,12 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingM import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode -/** Tests for [ExpressionToComparableOperationListConverter]. */ +/** Tests for [ExpressionToComparableOperationConverter]. */ // SameParameterValue: tests should have specific context included/excluded for readability. @Suppress("SameParameterValue") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) -class ExpressionToComparableOperationListConverterTest { +class ExpressionToComparableOperationConverterTest { // TODO: add high-level checks for the three types, but don't test in detail since there are // separate suites. Also, document the separate suites' existence in this suites's KDoc. From 28811b3d2f243a30bf8c1295f3cb7105e6ff4ea4 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 21:44:12 -0800 Subject: [PATCH 080/134] Add first round of tests. This includes fixes to the converter itself as it wasn't distributing both product inversions and negation correctly in several cases. Tests should now be covering these cases. --- ...xpressionToComparableOperationConverter.kt | 91 +- .../util/math/MathExpressionExtensions.kt | 2 +- .../org/oppia/android/util/math/BUILD.bazel | 2 +- ...ssionToComparableOperationConverterTest.kt | 1683 +++++++++++++++-- 4 files changed, 1566 insertions(+), 212 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt index 1dd214a78a2..5e9cc00b21f 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt @@ -28,6 +28,8 @@ import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.invertNegation +import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.toComparableOperation class ExpressionToComparableOperationConverter private constructor() { companion object { @@ -105,7 +107,7 @@ class ExpressionToComparableOperationConverter private constructor() { ComparableOperation.getDefaultInstance() } UNARY_OPERATION -> when (unaryOperation.operator) { - NEGATE -> unaryOperation.operand.toComparableOperation().makeNegative() + NEGATE -> unaryOperation.operand.toComparableOperation().invertNegation() POSITIVE -> unaryOperation.operand.toComparableOperation() UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> ComparableOperation.getDefaultInstance() @@ -140,8 +142,11 @@ class ExpressionToComparableOperationConverter private constructor() { commutativeAccumulation = CommutativeAccumulation.newBuilder().apply { accumulationType = PRODUCT val negativeCount = - addOperationToProduct(binaryOperation.leftOperand, forceInverse = false) + - addOperationToProduct(binaryOperation.rightOperand, forceInverse = isRhsInverted) + addOperationToProduct( + binaryOperation.leftOperand, forceInverse = false, invertNegation = false + ) + addOperationToProduct( + binaryOperation.rightOperand, forceInverse = isRhsInverted, invertNegation = false + ) // If an odd number of terms were negative then the overall product is negative. isNegated = (negativeCount % 2) != 0 sort() @@ -153,23 +158,27 @@ class ExpressionToComparableOperationConverter private constructor() { expression: MathExpression, forceNegative: Boolean ) { - when (expression.binaryOperation.operator) { - ADD -> { - // If the whole operation is negative, carry it to the left-hand side of the operation. + when { + expression.binaryOperation.operator == ADD -> { + // The whole operation being negative distributes to both sides of the addition. addOperationToSum(expression.binaryOperation.leftOperand, forceNegative) - addOperationToSum(expression.binaryOperation.rightOperand, forceNegative = false) + addOperationToSum(expression.binaryOperation.rightOperand, forceNegative) } - SUBTRACT -> { + expression.binaryOperation.operator == SUBTRACT -> { + // Similar to addition, negation distributes but is inverted by this subtraction for the + // right-hand operand. addOperationToSum(expression.binaryOperation.leftOperand, forceNegative) - addOperationToSum(expression.binaryOperation.rightOperand, forceNegative = true) - } - else -> when { - // Skip groups so that nested operations can be properly combined. - expression.expressionTypeCase == GROUP -> - addOperationToSum(expression.group, forceNegative) - forceNegative -> addCombinedOperations(expression.toComparableOperation().makeNegative()) - else -> addCombinedOperations(expression.toComparableOperation()) + addOperationToSum(expression.binaryOperation.rightOperand, !forceNegative) } + expression.unaryOperation.operator == NEGATE -> + addOperationToSum(expression.unaryOperation.operand, !forceNegative) + // Positive unary can be treated similarly to groups (inline for nesting). + expression.unaryOperation.operator == POSITIVE -> + addOperationToSum(expression.unaryOperation.operand, forceNegative) + // Skip groups so that nested operations can be properly combined. + expression.expressionTypeCase == GROUP -> addOperationToSum(expression.group, forceNegative) + forceNegative -> addCombinedOperations(expression.toComparableOperation().invertNegation()) + else -> addCombinedOperations(expression.toComparableOperation()) } } @@ -178,33 +187,55 @@ class ExpressionToComparableOperationConverter private constructor() { * collapsing subsequent products into a single list. * * @param forceInverse whether this expression is being divided rather than multiplied + * @param invertNegation whether to invert the negation sign for immediate constituent + * operations * @return the number of negative operations that were made positive before being added to the * accumulation */ private fun CommutativeAccumulation.Builder.addOperationToProduct( expression: MathExpression, - forceInverse: Boolean + forceInverse: Boolean, + invertNegation: Boolean ): Int { + // Note that negation only distributes "leftward" since subsequent right-hand operations would + // otherwise actually reverse the negation. return when { expression.binaryOperation.operator == MULTIPLY -> { - // If the whole operation is inverted, carry it to the left-hand side of the operation. - addOperationToProduct(expression.binaryOperation.leftOperand, forceInverse) + - addOperationToProduct(expression.binaryOperation.rightOperand, forceInverse = false) + // If the entire operation is inverted, that means each part of the multiplication should + // be, i.e.: 1/(x*y)=(1/x)*(1/y). + addOperationToProduct( + expression.binaryOperation.leftOperand, forceInverse, invertNegation + ) + addOperationToProduct( + expression.binaryOperation.rightOperand, forceInverse, invertNegation = false + ) } expression.binaryOperation.operator == DIVIDE -> { - addOperationToProduct(expression.binaryOperation.leftOperand, forceInverse) + - addOperationToProduct(expression.binaryOperation.rightOperand, forceInverse = true) + // Similar to multiplication, inversion for the whole operation results in distribution + // except the division inverts for the right-hand operand, i.e.: 1/(x/y)=(1/x)*y. + addOperationToProduct( + expression.binaryOperation.leftOperand, forceInverse, invertNegation + ) + addOperationToProduct( + expression.binaryOperation.rightOperand, !forceInverse, invertNegation = false + ) } + expression.unaryOperation.operator == NEGATE -> + addOperationToProduct(expression.unaryOperation.operand, forceInverse, !invertNegation) + // Positive unary can be treated similarly to groups (inline for nesting). + expression.unaryOperation.operator == POSITIVE -> + addOperationToProduct(expression.unaryOperation.operand, forceInverse, invertNegation) // Skip groups so that nested operations can be properly combined. expression.expressionTypeCase == GROUP -> - addOperationToProduct(expression.group, forceInverse) + addOperationToProduct(expression.group, forceInverse, invertNegation) else -> { val operationExpression = expression.toComparableOperation() - val positiveConvertedOperation = operationExpression.makePositive() + val potentiallyInvertedExpression = if (invertNegation) { + operationExpression.invertNegation() + } else operationExpression + val positiveConvertedOperation = potentiallyInvertedExpression.makePositive() if (forceInverse) { - addCombinedOperations(positiveConvertedOperation.makeInverted()) + addCombinedOperations(positiveConvertedOperation.invertInverted()) } else addCombinedOperations(positiveConvertedOperation) - if (operationExpression.isNegated) 1 else 0 + if (potentiallyInvertedExpression.isNegated) 1 else 0 } } } @@ -238,10 +269,10 @@ class ExpressionToComparableOperationConverter private constructor() { private fun ComparableOperation.makePositive(): ComparableOperation = toBuilder().apply { isNegated = false }.build() - private fun ComparableOperation.makeNegative(): ComparableOperation = - toBuilder().apply { isNegated = true }.build() + private fun ComparableOperation.invertNegation(): ComparableOperation = + toBuilder().apply { isNegated = !isNegated }.build() - private fun ComparableOperation.makeInverted(): ComparableOperation = - toBuilder().apply { isInverted = true }.build() + private fun ComparableOperation.invertInverted(): ComparableOperation = + toBuilder().apply { isInverted = !isInverted }.build() } } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 59268fca145..07b3d949fee 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -31,4 +31,4 @@ fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(div */ fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() -fun MathExpression.toComparableOperationList(): ComparableOperation = toComparableOperation() +fun MathExpression.toComparableOperation(): ComparableOperation = toComparableOperation() 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 index dbf95666418..cef614efcb0 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -66,7 +66,7 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", - "//testing/src/main/java/org/oppia/android/testing/math:comparable_operation_list_subject", + "//testing/src/main/java/org/oppia/android/testing/math:comparable_operation_subject", "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_truth", "//third_party:junit_junit", diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt index fd2607b1702..de0d3d391ea 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt @@ -1,69 +1,1426 @@ package org.oppia.android.util.math +import com.google.common.truth.Truth.assertThat import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Test +import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.toComparableOperation import org.junit.runner.RunWith -import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.PRODUCT -import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.SUMMATION +import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.PRODUCT +import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.SUMMATION import org.oppia.android.app.model.MathExpression -import org.oppia.android.testing.math.ComparableOperationListSubject.Companion.assertThat +import org.oppia.android.testing.math.ComparableOperationSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.REQUIRED_ONLY import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode /** Tests for [ExpressionToComparableOperationConverter]. */ -// SameParameterValue: tests should have specific context included/excluded for readability. -@Suppress("SameParameterValue") +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class ExpressionToComparableOperationConverterTest { - // TODO: add high-level checks for the three types, but don't test in detail since there are - // separate suites. Also, document the separate suites' existence in this suites's KDoc. + // TODO: add tests for comparator/sorting & negation simplification? + + /* Operation creation tests */ + + @Test + fun testConvert_integerConstantExpression_returnsConstantOperation() { + val expression = parseNumericExpression("2") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + + @Test + fun testConvert_decimalConstantExpression_returnsConstantOperation() { + val expression = parseNumericExpression("3.14") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + constantTerm { + withValueThat().isIrrationalThat().isWithin(1e-5).of(3.14) + } + } + } + + @Test + fun testConvert_variableExpression_returnsVariableOperation() { + val expression = parseAlgebraicExpression("x") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + + @Test + fun testConvert_addition_returnsSummation() { + val expression = parseNumericExpression("1+2") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + @Test + fun testConvert_addition_sameValues_returnsSummationWithBoth() { + val expression = parseNumericExpression("1+1") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + } + + @Test + fun testConvert_subtraction_returnsSummationOfNegative() { + val expression = parseNumericExpression("1-2") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + @Test + fun testConvert_multiplication_returnsProduct() { + val expression = parseNumericExpression("2*3") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(PRODUCT) { + index(0) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + @Test + fun testConvert_division_returnsProductOfInverted() { + val expression = parseNumericExpression("2/3") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(PRODUCT) { + index(0) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + @Test + fun testConvert_exponentiation_returnsNonCommutativeOperation() { + val expression = parseNumericExpression("2^3") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + nonCommutativeOperation { + exponentiation { + leftOperand { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + + @Test + fun testConvert_squareRoot_returnsNonCommutativeOperation() { + val expression = parseNumericExpression("sqrt(2)") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + nonCommutativeOperation { + squareRootWithArgument { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + @Test + fun testConvert_variableTerm_returnsNonNegativeOperation() { + val expression = parseAlgebraicExpression("x") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + } + } + + @Test + fun testConvert_negatedVariable_returnsNegativeVariableOperation() { + val expression = parseAlgebraicExpression("-x") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + + @Test + fun testConvert_positiveVariable_returnsVariableOperation() { + val expression = parseAlgebraicExpression("+x", errorCheckingMode = REQUIRED_ONLY) + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + + @Test + fun testConvert_positiveOfNegativeVariable_returnsNegativeVariableOperation() { + val expression = parseAlgebraicExpression("+-x", errorCheckingMode = REQUIRED_ONLY) + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + + @Test + fun testConvert_subtractionOfNegative_returnsSummationWithPositives() { + val expression = parseNumericExpression("1--2") + + val comparable = expression.toComparableOperation() + + // Verify that the subtraction & negation cancel out each other. + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + @Test + fun testConvert_negativePlusPositive_returnsSummationWithFirstTermNegative() { + val expression = parseNumericExpression("-2+1") + + val comparable = expression.toComparableOperation() + + // Verify that the negative only applies to the 2, not to the whole expression. + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + @Test + fun testConvert_multipleAdditions_returnsCombinedSummation() { + val expression = parseNumericExpression("1+2+3") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + @Test + fun testConvert_multipleSubtractions_returnsCombinedSummation() { + val expression = parseNumericExpression("1-2-3") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + @Test + fun testConvert_additionsAndSubtractions_returnsCombinedSummation() { + val expression = parseNumericExpression("1+2-3-4+5") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + index(3) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(4) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + + @Test + fun testConvert_additionsWithNestedAdds_returnsCompletelyCombinedSummation() { + val expression = parseNumericExpression("1+((2+(3+4)+5)+6)") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(6) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(4) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + index(5) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + } + } + } + + @Test + fun testConvert_subtractsWithNesting_returnsSummationWithDistributedNegation() { + val expression = parseNumericExpression("1-(2+3-4)") + + val comparable = expression.toComparableOperation() + + // Both the 2 & 3 are negative since the subtraction distributes, and the 4 becomes positive. + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(4) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(2) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(3) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + @Test + fun testConvert_subtractsWithNestedSubs_returnsCompletelyCombinedSummation() { + val expression = parseNumericExpression("1-((2-(3-4)-5)-6)") + + val comparable = expression.toComparableOperation() + + // Some of these are positive because of distribution. + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(6) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + index(4) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(5) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + + @Test + fun testConvert_additionsAndSubtractionsWithNested_returnsCombinedSummation() { + val expression = parseNumericExpression("1++(2-3)+-(4+5--(2+3-1))", REQUIRED_ONLY) + + val comparable = expression.toComparableOperation() + + // This also verifies that negation distributes in the same way as subtraction. + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(8) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(3) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(4) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(5) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(6) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(7) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + + @Test + fun testConvert_multipleMultiplications_returnsCombinedProduct() { + val expression = parseNumericExpression("2*3*4") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + + @Test + fun testConvert_multipleDivisions_returnsCombinedProduct() { + val expression = parseNumericExpression("2/3/4") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + + @Test + fun testConvert_multiplicationsAndDivisions_returnsCombinedProduct() { + val expression = parseNumericExpression("2*3/4/5*6") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + index(3) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(4) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + + @Test + fun testConvert_multiplicationsWithNestedMults_returnsCompletelyCombinedProduct() { + val expression = parseNumericExpression("2*((3*(4*5)*6)*7)") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(6) + index(0) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(3) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + index(4) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + index(5) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + } + + @Test + fun testConvert_dividesWithNesting_returnsProductWithDistributedInversion() { + val expression = parseNumericExpression("2/(3*4/5)") + + val comparable = expression.toComparableOperation() + + // Both the 3 & 5 become inverted, and the 5 becomes regular multiplication due to the division + // distribution. + // 2*5*inv(3)*inv(4) + assertThat(comparable).hasStructureThatMatches { + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(4) + index(0) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + index(2) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(3) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + + @Test + fun testConvert_divisionsWithNestedDivs_returnsCompletelyCombinedProduct() { + val expression = parseNumericExpression("2/((3/(4/5)/6)/7)") + + val comparable = expression.toComparableOperation() + + // Some of these are non-inverted because of distribution. + assertThat(comparable).hasStructureThatMatches { + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(6) + index(0) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(2) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + index(3) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + index(4) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(5) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + + @Test + fun testConvert_multiplicationsAndDivisionsWithNested_returnsCombinedProduct() { + val expression = parseNumericExpression("1*(2/3)/(4*5*(2*3/1))") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(8) + index(0) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(3) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(4) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(5) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(6) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(7) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + + @Test + fun testConvert_multiplicationWithNoNegatives_returnsPositiveProduct() { + val expression = parseNumericExpression("2*3") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + } + } + } + } + + @Test + fun testConvert_multiplicationWithOneNegative_returnsNegativeProduct() { + val expression = parseNumericExpression("2*-3") + + val comparable = expression.toComparableOperation() + + // The entire accumulation is considered negative. + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + @Test + fun testConvert_multiplicationWithTwoNegatives_returnsPositiveProduct() { + val expression = parseNumericExpression("-2*-3") + + val comparable = expression.toComparableOperation() + + // The two negatives cancel out. This also verifies that negation can pipe up to top-level + // negation. + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + @Test + fun testConvert_multiplicationWithThreeNegatives_returnsNegativeProduct() { + val expression = parseNumericExpression("-2*-3*-4") + + val comparable = expression.toComparableOperation() + + // 3 negative operands results in the overall product being negative. + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + + @Test + fun testConvert_combinedMultDivWithNested_evenNegatives_returnsPositiveProduct() { + val expression = parseNumericExpression("-2*-3/-(4/-(3*2))") + + val comparable = expression.toComparableOperation() + + // There are four negatives, so the overall expression is positive. + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + } + } + + @Test + fun testConvert_combinedMultDivWithNested_oddNegatives_returnsNegativeProduct() { + val expression = parseNumericExpression("-2*-3/-(4/-(3*2*+(-3*7)))", REQUIRED_ONLY) + + val comparable = expression.toComparableOperation() + + // There are five negatives, so the overall expression is negative. Note that this is also + // verifying that the negation properly distributes with the group. + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + // This is a side extra check to ensure that unary positive groups are correctly folded into + // the product. + hasOperandCountThat().isEqualTo(7) + } + } + } + + @Test + fun testConvert_additionAndExp_returnsSummationWithNonCommutative() { + val expression = parseNumericExpression("1+2^3") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + nonCommutativeOperation { + exponentiation { + leftOperand { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + } - // TODO: use utility directly - // TODO: finish tests. - // TODO: add tests for comparator/sorting & negation simplification? + @Test + fun testConvert_additionAndSquareRoot_returnsSummationWithNonCommutative() { + val expression = parseNumericExpression("1+sqrt(2)") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + nonCommutativeOperation { + squareRootWithArgument { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + } + + @Test + fun testConvert_additionWithinExp_returnsSummationWithinNonCommutative() { + val expression = parseNumericExpression("2^(1+3)") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + nonCommutativeOperation { + exponentiation { + leftOperand { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } + } + + @Test + fun testConvert_additionWithinSquareRoot_returnsSummationWithinNonCommutative() { + val expression = parseNumericExpression("sqrt(1+3)") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + nonCommutativeOperation { + squareRootWithArgument { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } + + @Test + fun testConvert_multiplicationAndExp_returnsProductWithNonCommutative() { + val expression = parseNumericExpression("2*3^4") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + nonCommutativeOperation { + exponentiation { + leftOperand { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + @Test + fun testConvert_multiplicationAndSquareRoot_returnsProductWithNonCommutative() { + val expression = parseNumericExpression("2*sqrt(3)") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + nonCommutativeOperation { + squareRootWithArgument { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + @Test + fun testConvert_multiplicationWithinExp_returnsProductWithinNonCommutative() { + val expression = parseNumericExpression("2^(3*4)") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + nonCommutativeOperation { + exponentiation { + leftOperand { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + } + + @Test + fun testConvert_multiplicationWithinSquareRoot_returnsProductWithinNonCommutative() { + val expression = parseNumericExpression("sqrt(2*3)") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + nonCommutativeOperation { + squareRootWithArgument { + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } - /* Operation creation */ - // testConvert_constantExpression_returnsConstantOperation - // testConvert_variableExpression_returnsVariableOperation - // testConvert_addition_returnsSummation - // testConvert_subtraction_returnsSummationOfNegative - // testConvert_multiplication_returnsProduct - // testConvert_division_returnsProductOfInverted - // testConvert_exponentiation_returnsNonCommutativeOperation - // testConvert_squareRoot_returnsNonCommutativeOperation - // testConvert_negatedVariable_returnsNegativeVariableOperation - // testConvert_positiveVariable_returnsVariableOperation - // testConvert_positiveOfNegativeVariable_returnsNegativeVariableOperation - // testConvert_subtractionOfNegative_returnsSummationWithPositives - // testConvert_multipleAdditions_returnsCombinedSummation - // testConvert_multipleSubtractions_returnsCombinedSummation - // testConvert_additionsAndSubtractions_returnsCombinedSummation - // testConvert_additionsWithNestedAdds_returnsCompletelyCombinedSummation - // testConvert_subtractsWithNestedSubs_returnsCompletelyCombinedSummation - // testConvert_additionsAndSubtractionsWithNested_returnsCombinedSummation - // testConvert_multipleMultiplications_returnsCombinedProduct - // testConvert_multipleDivisions_returnsCombinedProduct - // testConvert_multiplicationsAndDivisions_returnsCombinedProduct - // testConvert_multiplicationsWithNestedMults_returnsCompletelyCombinedProduct - // testConvert_divisionsWithNestedDivs_returnsCompletelyCombinedProduct - // testConvert_multiplicationsAndDivisionsWithNested_returnsCombinedProduct - // testConvert_multiplicationWithOneNegative_returnsNegativeProduct - // testConvert_multiplicationWithTwoNegatives_returnsPositiveProduct - // testConvert_multiplicationWithThreeNegatives_returnsNegativeProduct - // testConvert_combinedMultDivWithNested_evenNegatives_returnsPositiveProduct - // testConvert_combinedMultDivWithNested_oddNegatives_returnsNegativeProduct - // testConvert_additionAndExp_returnsSummationWithNonCommutative - // testConvert_additionAndSquareRoot_returnsSummationWithNonCommutative - // testConvert_additionWithinExp_returnsSummationWithinNonCommutative - // testConvert_additionWithinSquareRoot_returnsSummationWithinNonCommutative - // testConvert_multiplicationAndExp_returnsProductWithNonCommutative - // testConvert_multiplicationAndSquareRoot_returnsProductWithNonCommutative - // testConvert_multiplicationWithinExp_returnsProductWithinNonCommutative - // testConvert_multiplicationWithinSquareRoot_returnsProductWithinNonCommutative - // testConvert_additionAndMultiplication_returnsSummationOfProduct - // testConvert_multiplicationAndGroupedAddition_returnsProductOfSummation + @Test + fun testConvert_additionAndMultiplication_returnsSummationOfProduct() { + val expression = parseNumericExpression("2*3+1") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + } + + @Test + fun testConvert_multiplicationAndGroupedAddition_returnsProductOfSummation() { + val expression = parseNumericExpression("2*(3+1)") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } /* Top-level operation sorting */ // testConvert_additionThenSquareRoot_samePrecedence_returnsOpWithSummationFirst @@ -80,6 +1437,7 @@ class ExpressionToComparableOperationConverterTest { // testConvert_twoVariables_invertedThenNegated_returnsOpWithNegatedFirst /* Accumulator sorting */ + // TODO: add sorting for negatives & inverteds. // TODO: mention no tiebreakers since there can't be summations or products adjacent with others // of the same type. // testConvert_additionAndMult_samePrecedence_returnsOpWithSummationFirst @@ -181,8 +1539,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test1() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("1") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() constantTerm { @@ -194,8 +1552,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test2() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("-1") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("-1") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isTrue() hasInvertedPropertyThat().isFalse() constantTerm { @@ -207,8 +1565,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test3() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+3+4") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("1+3+4") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -241,8 +1599,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test4() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("-1-2-3") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("-1-2-3") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -275,8 +1633,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test5() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+2-3") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("1+2-3") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -309,8 +1667,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test6() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("2*3*4") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("2*3*4") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -343,8 +1701,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test7() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1-2*3") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("1-2*3") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -384,8 +1742,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test8() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("2*3-4") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("2*3-4") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -425,8 +1783,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test9() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+2*3-4+8*7*6-9") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("1+2*3-4+8*7*6-9") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -508,8 +1866,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test10() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("2/3/4") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("2/3/4") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -542,8 +1900,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test11() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithoutOptionalErrors("2^3^4") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("2^3^4", REQUIRED_ONLY) + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() nonCommutativeOperation { @@ -585,8 +1943,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test12() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+2/3+3") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("1+2/3+3") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -633,8 +1991,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test13() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+(2/3)+3") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("1+(2/3)+3") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -681,8 +2039,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test14() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+2^3+3") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("1+2^3+3") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -730,8 +2088,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test15() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+(2^3)+3") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("1+(2^3)+3") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -779,8 +2137,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test16() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("2*3/4*7") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("2*3/4*7") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -820,8 +2178,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test17() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("2*(3/4)*7") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("2*(3/4)*7") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -861,8 +2219,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test18() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("-3*sqrt(2)") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("-3*sqrt(2)") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isTrue() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -894,8 +2252,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test19() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+(2+(3+(4+5)))") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("1+(2+(3+(4+5)))") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -942,8 +2300,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test20() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("2*(3*(4*(5*6)))") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("2*(3*(4*(5*6)))") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -990,8 +2348,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test21() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("x") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() variableTerm { @@ -1003,8 +2361,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test22() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("-x") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("-x") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isTrue() hasInvertedPropertyThat().isFalse() variableTerm { @@ -1016,8 +2374,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test23() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x+y") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("1+x+y") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1050,8 +2408,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test24() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("-1-x-y") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("-1-x-y") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1084,8 +2442,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test25() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x-y") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("1+x-y") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1118,8 +2476,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test26() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2xy") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("2xy") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -1152,8 +2510,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test27() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1-xy") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("1-xy") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1193,8 +2551,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test28() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy-4") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("xy-4") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1234,8 +2592,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test29() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+xy-4+yz-9") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("1+xy-4+yz-9") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1310,8 +2668,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test30() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2/x/y") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("2/x/y") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -1344,8 +2702,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test31() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("x^3^4") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("x^3^4", REQUIRED_ONLY) + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() nonCommutativeOperation { @@ -1387,8 +2745,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test32() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x/y+z") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("1+x/y+z") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1435,8 +2793,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test33() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x/y)+z") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("1+(x/y)+z") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1483,8 +2841,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test34() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x^3+y") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("1+x^3+y") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1532,8 +2890,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test35() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x^3)+y") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("1+(x^3)+y") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1581,8 +2939,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test36() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*x/y*z") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("2*x/y*z") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -1622,8 +2980,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test37() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(x/y)*z") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("2*(x/y)*z") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -1663,8 +3021,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test38() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("-2*sqrt(x)") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("-2*sqrt(x)") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isTrue() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -1696,8 +3054,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test39() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x+(3+(z+y)))") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("1+(x+(3+(z+y)))") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1744,8 +3102,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test40() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(x*(4*(zy)))") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("2*(x*(4*(zy)))") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -1975,66 +3333,31 @@ class ExpressionToComparableOperationConverterTest { } private fun createComparableOperationListFromNumericExpression(expression: String) = - parseNumericExpressionSuccessfullyWithAllErrors(expression).toComparableOperationList() + parseNumericExpression(expression).toComparableOperation() private fun createComparableOperationListFromAlgebraicExpression(expression: String) = - parseAlgebraicExpressionSuccessfullyWithAllErrors(expression).toComparableOperationList() + parseAlgebraicExpression(expression).toComparableOperation() private companion object { - // TODO: fix helper API. - - private fun parseNumericExpressionSuccessfullyWithAllErrors( - expression: String + private fun parseNumericExpression( + expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS ): MathExpression { - val result = parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) - return (result as MathParsingResult.Success).result + return MathExpressionParser.parseNumericExpression( + expression, errorCheckingMode + ).retrieveExpectedSuccessfulResult() } - private fun parseNumericExpressionSuccessfullyWithoutOptionalErrors( - expression: String + private fun parseAlgebraicExpression( + expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS ): MathExpression { - val result = - parseNumericExpressionInternal( - expression, ErrorCheckingMode.REQUIRED_ONLY - ) - return (result as MathParsingResult.Success).result - } - - private fun parseNumericExpressionInternal( - expression: String, - errorCheckingMode: ErrorCheckingMode - ): MathParsingResult { - return MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) - } - - private fun parseAlgebraicExpressionSuccessfullyWithAllErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathExpression { - val result = - parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) - return (result as MathParsingResult.Success).result + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables = listOf("x", "y", "z"), errorCheckingMode + ).retrieveExpectedSuccessfulResult() } - private fun parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathExpression { - val result = - parseAlgebraicExpressionInternal( - expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables - ) - return (result as MathParsingResult.Success).result - } - - private fun parseAlgebraicExpressionInternal( - expression: String, - errorCheckingMode: ErrorCheckingMode, - allowedVariables: List = listOf("x", "y", "z") - ): MathParsingResult { - return MathExpressionParser.parseAlgebraicExpression( - expression, allowedVariables, errorCheckingMode - ) + private fun MathParsingResult.retrieveExpectedSuccessfulResult(): T { + assertThat(this).isInstanceOf(MathParsingResult.Success::class.java) + return (this as MathParsingResult.Success).result } } } From b4001767fd3ea16b4f872e8d4819690959397cc3 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 28 Jan 2022 17:12:20 -0800 Subject: [PATCH 081/134] Finish initial test suite. Still needs to be cleaned up, but after converter refactoring attempts. --- .../org/oppia/android/util/math/BUILD.bazel | 3 +- ...ssionToComparableOperationConverterTest.kt | 1438 +++++++++++++++-- 2 files changed, 1321 insertions(+), 120 deletions(-) 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 index cef614efcb0..a6fe0270cea 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -67,10 +67,11 @@ oppia_android_test( deps = [ "//model/src/main/proto:math_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/math:comparable_operation_subject", - "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", "//third_party:robolectric_android-all", "//utility/src/main/java/org/oppia/android/util/math:extensions", "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt index de0d3d391ea..5d4dc8495fe 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt @@ -1,13 +1,16 @@ package org.oppia.android.util.math import com.google.common.truth.Truth.assertThat -import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Test import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.toComparableOperation import org.junit.runner.RunWith import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.PRODUCT import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.SUMMATION import org.oppia.android.app.model.MathExpression +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized import org.oppia.android.testing.math.ComparableOperationSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS @@ -15,13 +18,19 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingM import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode -/** Tests for [ExpressionToComparableOperationConverter]. */ +/** + * Tests for [ExpressionToComparableOperationConverter]. + * + * Note that this suite is broken up into distinct sections (designated by block comments) to better + * help organize the different behaviors being tested. + */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") -@RunWith(AndroidJUnit4::class) +@RunWith(OppiaParameterizedTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class ExpressionToComparableOperationConverterTest { - // TODO: add tests for comparator/sorting & negation simplification? + @Parameter lateinit var op1: String + @Parameter lateinit var op2: String /* Operation creation tests */ @@ -586,7 +595,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testConvert_additionsAndSubtractionsWithNested_returnsCombinedSummation() { - val expression = parseNumericExpression("1++(2-3)+-(4+5--(2+3-1))", REQUIRED_ONLY) + val expression = + parseNumericExpression("1++(2-3)+-(4+5--(2+3-1))", errorCheckingMode = REQUIRED_ONLY) val comparable = expression.toComparableOperation() @@ -1095,7 +1105,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testConvert_combinedMultDivWithNested_oddNegatives_returnsNegativeProduct() { - val expression = parseNumericExpression("-2*-3/-(4/-(3*2*+(-3*7)))", REQUIRED_ONLY) + val expression = + parseNumericExpression("-2*-3/-(4/-(3*2*+(-3*7)))", errorCheckingMode = REQUIRED_ONLY) val comparable = expression.toComparableOperation() @@ -1422,119 +1433,1306 @@ class ExpressionToComparableOperationConverterTest { } } - /* Top-level operation sorting */ - // testConvert_additionThenSquareRoot_samePrecedence_returnsOpWithSummationFirst - // testConvert_squareRootThenAddition_samePrecedence_returnsOpWithSummationFirst - // testConvert_additionThenExp_samePrecedence_returnsOpWithSummationFirst - // testConvert_exponentiationThenAddition_samePrecedence_returnsOpWithSummationFirst - // testConvert_constantThenSquareRoot_samePrecedence_returnsOpWithNonCommutativeFirst - // testConvert_squareRootThenConstant_samePrecedence_returnsOpWithNonCommutativeFirst - // testConvert_constantThenExp_samePrecedence_returnsOpWithNonCommutativeFirst - // testConvert_exponentiationThenConstant_samePrecedence_returnsOpWithNonCommutativeFirst - // testConvert_constantThenVariable_samePrecedence_returnsOpWithConstantFirst - // testConvert_variableThenConstant_samePrecedence_returnsOpWithConstantFirst - // testConvert_twoVariables_negatedThenInverted_returnsOpWithNegatedFirst - // testConvert_twoVariables_invertedThenNegated_returnsOpWithNegatedFirst + /* + * Top-level operation sorting. Note that negation & inversion can't be sorted relative to each + * other since they'll never co-occur (though the underlying sorting is set up to prioritize + * negative operations over inverted). + * + * Note also that accumulators can't be cross-verified for order since whether a summation or + * product is first entirely depends on the expression itself (since multiplication and division + * are higher precedence than addition and subtraction). Thus, these cases can't be tested for + * sorting order. + */ - /* Accumulator sorting */ - // TODO: add sorting for negatives & inverteds. - // TODO: mention no tiebreakers since there can't be summations or products adjacent with others - // of the same type. - // testConvert_additionAndMult_samePrecedence_returnsOpWithSummationFirst - // testConvert_multiplicationAndAddition_samePrecedence_returnsOpWithSummationFirst - // testConvert_additionAndMult_samePrecedenceAsNested_returnsOpWithSummationFirst - // testConvert_multiplicationAndAddition_samePrecedenceAsNested_returnsOpWithSummationFirst + @Test + @RunParameterized( + Iteration(name = "(1+2)*sqrt(3)", "op1=(1+2)", "op2=sqrt(3)"), + Iteration(name = "sqrt(3)*(1+2)", "op1=sqrt(3)", "op2=(1+2)"), + Iteration(name = "(1+2)*(3^4)", "op1=(1+2)", "op2=(3^4)"), + Iteration(name = "(3^4)*(1+2)", "op1=(3^4)", "op2=(1+2)") + ) + fun testConvert_additionAndNonCommutativeOp_samePrecedence_returnsOpWithSummationFirst() { + val expression = parseNumericExpression("$op1 * $op2") + + val comparable = expression.toComparableOperation() + + // Verify that the summation is still first since it's higher priority during sorting. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + commutativeAccumulationWithType(SUMMATION) {} + } + index(1) { + nonCommutativeOperation {} + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "2+sqrt(3)", "op1=2", "op2=sqrt(3)"), + Iteration(name = "sqrt(3)+2", "op1=sqrt(3)", "op2=2"), + Iteration(name = "2+3^4", "op1=2", "op2=3^4"), + Iteration(name = "3^4+2", "op1=3^4", "op2=2") + ) + fun testConvert_constantAndNonCommutativeOp_samePrecedence_returnsOpWithNonCommutativeFirst() { + val expression = parseNumericExpression("$op1 + $op2") + + val comparable = expression.toComparableOperation() + + // Verify that the non-commutative operation is first since it's higher priority during sorting. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + nonCommutativeOperation {} + } + index(1) { + constantTerm {} + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "2*x", "op1=2", "op2=x"), + Iteration(name = "x*2", "op1=x", "op2=2") + ) + fun testConvert_constantAndVariable_samePrecedence_returnsOpWithConstantFirst() { + val expression = parseAlgebraicExpression("$op1 * $op2") + + val comparable = expression.toComparableOperation() + + // Verify that the constant is first since it's higher priority during sorting. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm {} + } + index(1) { + variableTerm {} + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "x+(-y)", "op1=x", "op2=(-y)"), + Iteration(name = "(-y)+x", "op1=(-y)", "op2=x") + ) + fun testConvert_positiveAndNegativeVariables_returnsOpWithNegatedLast() { + val expression = parseAlgebraicExpression("$op1 + $op2") + + val comparable = expression.toComparableOperation() + + // Verify that the positive term is first since it's higher priority during sorting. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + variableTerm {} + } + index(1) { + hasNegatedPropertyThat().isTrue() + variableTerm {} + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "x*(1/y)", "op1=x", "op2=(1/y)"), + Iteration(name = "(1/y)*x", "op1=(1/y)", "op2=x") + ) + fun testConvert_invertedAndNonInvertedVariables_returnsOpWithInvertedLast() { + val expression = parseAlgebraicExpression("$op1 * $op2") + + val comparable = expression.toComparableOperation() + + // Verify that the non-inverted term is first since it's higher priority during sorting. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasInvertedPropertyThat().isFalse() + constantTerm {} + } + index(1) { + hasInvertedPropertyThat().isFalse() + variableTerm {} + } + index(2) { + hasInvertedPropertyThat().isTrue() + variableTerm {} + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "(1+2)*(2+3)", "op1=1+2", "op2=2+3"), + Iteration(name = "(2+1)*(2+3)", "op1=2+1", "op2=2+3"), + Iteration(name = "(1+2)*(3+2)", "op1=1+2", "op2=3+2"), + Iteration(name = "(2+1)*(3+2)", "op1=2+1", "op2=3+2"), + Iteration(name = "(2+3)*(1+2)", "op1=2+3", "op2=1+2"), + Iteration(name = "(2+3)*(2+1)", "op1=2+3", "op2=2+1"), + Iteration(name = "(3+2)*(1+2)", "op1=3+2", "op2=1+2"), + Iteration(name = "(3+2)*(2+1)", "op1=3+2", "op2=2+1") + ) + fun testConvert_twoAdditionsInProduct_smallerSumIsFirst() { + val expression = parseNumericExpression("($op1)*($op2)") + + val comparable = expression.toComparableOperation() + + // Summations are deterministically sorted regardless of how the original expression structures + // them. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + index(1) { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "(2*3)+(4*5)", "op1=2*3", "op2=4*5"), + Iteration(name = "(3*2)+(4*5)", "op1=3*2", "op2=4*5"), + Iteration(name = "(2*3)+(5*4)", "op1=2*3", "op2=5*4"), + Iteration(name = "(3*2)+(5*4)", "op1=3*2", "op2=5*4"), + Iteration(name = "(4*5)+(2*3)", "op1=4*5", "op2=2*3"), + Iteration(name = "(4*5)+(3*2)", "op1=4*5", "op2=3*2"), + Iteration(name = "(5*4)+(2*3)", "op1=5*4", "op2=2*3"), + Iteration(name = "(5*4)+(3*2)", "op1=5*4", "op2=3*2") + ) + fun testConvert_twoMultiplicationsInSum_smallerProductIsFirst() { + val expression = parseNumericExpression("($op1)+($op2)") + + val comparable = expression.toComparableOperation() + + // Products are deterministically sorted regardless of how the original expression structures + // them. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + index(1) { + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + /* Non-commutative sorting */ + + @Test + @RunParameterized( + Iteration(name = "(2^3)+sqrt(2)", "op1=(2^3)", "op2=sqrt(2)"), + Iteration(name = "sqrt(2)+(2^3)", "op1=sqrt(2)", "op2=(2^3)") + ) + fun testConvert_expAndSqrt_samePrecedence_returnsOpWithExpThenSqrt() { + val expression = parseNumericExpression("$op1+$op2") + + val comparable = expression.toComparableOperation() + + // Verify that the exponentiation is first since it's higher priority during sorting. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + nonCommutativeOperation { + exponentiation {} + } + } + index(1) { + nonCommutativeOperation { + squareRootWithArgument {} + } + } + } + } + } + + @Test + @RunParameterized( + // const^const + const^const + Iteration(name = "(2^3)+(4^5)", "op1=2^3", "op2=4^5"), + Iteration(name = "(2^5)+(4^3)", "op1=2^5", "op2=4^3"), + Iteration(name = "(4^3)+(2^5)", "op1=4^3", "op2=2^5"), + Iteration(name = "(4^5)+(2^3)", "op1=4^5", "op2=2^3"), + // const^var + const^var + Iteration(name = "(2^x)+(4^5)", "op1=2^x", "op2=4^5"), + Iteration(name = "(2^5)+(4^x)", "op1=2^5", "op2=4^x"), + Iteration(name = "(4^x)+(2^5)", "op1=4^x", "op2=2^5"), + Iteration(name = "(4^5)+(2^x)", "op1=4^5", "op2=2^x"), + // const^(var or const) + const^(const or var) + Iteration(name = "(2^x)+(4^y)", "op1=2^x", "op2=4^y"), + Iteration(name = "(2^y)+(4^x)", "op1=2^y", "op2=4^x"), + Iteration(name = "(4^x)+(2^y)", "op1=4^x", "op2=2^y"), + Iteration(name = "(4^y)+(2^x)", "op1=4^y", "op2=2^x") + ) + fun testConvert_addTwoExps_lhs1Const_rhs1Any_lhs2Const_rhs2Any_returnsOpWithLhsSizeBasedOrder() { + // Note that optional errors need to be disabled as part of testing exponents as powers. + val expression = + parseAlgebraicExpression("($op1)+($op2)", errorCheckingMode = REQUIRED_ONLY) + + val comparable = expression.toComparableOperation() + + // Verify that the exponentiations are ordered based on the left-hand operand's size. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + nonCommutativeOperation { + exponentiation { + leftOperand { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + index(1) { + nonCommutativeOperation { + exponentiation { + leftOperand { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + } + + @Test + @RunParameterized( + // var^const + var^const + Iteration(name = "(u^3)+(v^5)", "op1=u^3", "op2=v^5"), + Iteration(name = "(u^5)+(v^3)", "op1=u^5", "op2=v^3"), + Iteration(name = "(v^3)+(u^5)", "op1=v^3", "op2=u^5"), + Iteration(name = "(v^5)+(u^3)", "op1=v^5", "op2=u^3"), + // var^var + var^var + Iteration(name = "(u^x)+(v^5)", "op1=u^x", "op2=v^5"), + Iteration(name = "(u^5)+(v^x)", "op1=u^5", "op2=v^x"), + Iteration(name = "(v^x)+(u^5)", "op1=v^x", "op2=u^5"), + Iteration(name = "(v^5)+(u^x)", "op1=v^5", "op2=u^x"), + // var^(var or const) + var^(const or var) + Iteration(name = "(u^x)+(v^y)", "op1=u^x", "op2=v^y"), + Iteration(name = "(u^y)+(v^x)", "op1=u^y", "op2=v^x"), + Iteration(name = "(v^x)+(u^y)", "op1=v^x", "op2=u^y"), + Iteration(name = "(v^y)+(u^x)", "op1=v^y", "op2=u^x") + ) + fun testConvert_addTwoExps_lhs1Var_rhs1Any_lhs2Var_rhs2Any_returnsOpWithLhsLetterBasedOrder() { + // Note that optional errors need to be disabled as part of testing exponents as powers. + val expression = + parseAlgebraicExpression( + "($op1)+($op2)", + allowedVariables = listOf("u", "v", "x", "y"), + errorCheckingMode = REQUIRED_ONLY + ) + + val comparable = expression.toComparableOperation() + + // Verify that the exponentiations are ordered based on the left-hand operand's lexicographical + // ordering. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + nonCommutativeOperation { + exponentiation { + leftOperand { + variableTerm { + withNameThat().isEqualTo("u") + } + } + } + } + } + index(1) { + nonCommutativeOperation { + exponentiation { + leftOperand { + variableTerm { + withNameThat().isEqualTo("v") + } + } + } + } + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "sqrt(2)+sqrt(3)", "op1=2", "op2=3"), + Iteration(name = "sqrt(3)+sqrt(2)", "op1=3", "op2=2") + ) + fun testConvert_addTwoSqrts_leftConst_rightConst_returnsOpWithSqrtsByArgSize() { + val expression = parseNumericExpression("sqrt($op1)+sqrt($op2)") + + val comparable = expression.toComparableOperation() + + // The square roots should be ordered based on their argument sorting. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + nonCommutativeOperation { + squareRootWithArgument { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + index(1) { + nonCommutativeOperation { + squareRootWithArgument { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "sqrt(x)+sqrt(y)", "op1=x", "op2=y"), + Iteration(name = "sqrt(y)+sqrt(x)", "op1=y", "op2=x") + ) + fun testConvert_addTwoSqrts_leftVar_rightVar_returnsOpWithSqrtsByVariableOrder() { + val expression = parseAlgebraicExpression("sqrt($op1)+sqrt($op2)") + + val comparable = expression.toComparableOperation() + + // The square roots should be ordered based on their argument lexicographical sorting. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + nonCommutativeOperation { + squareRootWithArgument { + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + } + index(1) { + nonCommutativeOperation { + squareRootWithArgument { + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "sqrt(2)+sqrt(x)", "op1=2", "op2=x"), + Iteration(name = "sqrt(x)+sqrt(2)", "op1=x", "op2=2") + ) + fun testConvert_addTwoSqrts_oneConst_oneVar_returnsOpWithSqrtsByConstFirst() { + val expression = parseAlgebraicExpression("sqrt($op1)+sqrt($op2)") + + val comparable = expression.toComparableOperation() + + // Constant-before-variable ordering also affects peer square root orders. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + nonCommutativeOperation { + squareRootWithArgument { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + index(1) { + nonCommutativeOperation { + squareRootWithArgument { + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + } + } + } + } + + /* Constant & variable sorting */ + + @Test + @RunParameterized( + Iteration(name = "2+3", "op1=2", "op2=3"), + Iteration(name = "3+2", "op1=3", "op2=2") + ) + fun testConvert_addTwoConstants_leftInteger_rightInteger_returnsOpSortedByValues() { + val expression = parseNumericExpression("$op1 + $op2") + + val comparable = expression.toComparableOperation() + + // The order of the summation should be based on the constants' values. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "3.2+6.3", "op1=3.2", "op2=6.3"), + Iteration(name = "6.3+3.2", "op1=6.3", "op2=3.2") + ) + fun testConvert_addTwoConstants_leftDouble_rightDouble_returnsOpSortedByValues() { + val expression = parseNumericExpression("$op1 + $op2") + + val comparable = expression.toComparableOperation() + + // The order of the summation should be based on the constants' values. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIrrationalThat().isWithin(1e-5).of(3.2) + } + } + index(1) { + constantTerm { + withValueThat().isIrrationalThat().isWithin(1e-5).of(6.3) + } + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "3+6.3", "op1=3", "op2=6.3"), + Iteration(name = "6.3+3", "op1=6.3", "op2=3") + ) + fun testConvert_addTwoConstants_smallInt_largeDouble_returnsOpWithIntFirst() { + val expression = parseNumericExpression("$op1 + $op2") + + val comparable = expression.toComparableOperation() + + // The order of the summation should be based on the constants' values. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(1) { + constantTerm { + withValueThat().isIrrationalThat().isWithin(1e-5).of(6.3) + } + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "8+6.3", "op1=8", "op2=6.3"), + Iteration(name = "6.3+8", "op1=6.3", "op2=8") + ) + fun testConvert_addTwoConstants_largeInt_smallDouble_returnsOpWithDoubleFirst() { + val expression = parseNumericExpression("$op1 + $op2") + + val comparable = expression.toComparableOperation() + + // The order of the summation should be based on the constants' values. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIrrationalThat().isWithin(1e-5).of(6.3) + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(8) + } + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "x+6", "op1=x", "op2=6"), + Iteration(name = "6+x", "op1=6", "op2=x") + ) + fun testConvert_addVarAndIntConstant_returnsOpWithConstantFirst() { + val expression = parseAlgebraicExpression("$op1 + $op2") + + val comparable = expression.toComparableOperation() + + // Constants are always ordered before variables. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + index(1) { + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "3.6+x", "op1=3.6", "op2=x"), + Iteration(name = "x+3.6", "op1=x", "op2=3.6") + ) + fun testConvert_addVarAndDoubleConstant_returnsOpWithConstantFirst() { + val expression = parseAlgebraicExpression("$op1 + $op2") + + val comparable = expression.toComparableOperation() + + // The order of the summation should be based on the constants' values. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIrrationalThat().isWithin(1e-5).of(3.6) + } + } + index(1) { + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + } + } + + @Test + fun testConvert_addTwoVariables_leftX_rightX_returnsOpBothXs() { + val expression = parseAlgebraicExpression("x + x") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "x+y", "op1=x", "op2=y"), + Iteration(name = "y+x", "op1=y", "op2=x") + ) + fun testConvert_addTwoVariables_oneX_oneY_returnsOpWithXThenY() { + val expression = parseAlgebraicExpression("$op1 + $op2") + + val comparable = expression.toComparableOperation() + + // Variables are sorted lexicographically. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + } + + @Test + fun testConvert_addMultipleVars_returnsOpWithThemInOrder() { + val expression = parseAlgebraicExpression("x + z + x + y + x") - /* Non-commutative sorting */ - // testConvert_addExpThenSqrt_samePrecedence_returnsOpWithExpThenSqrt - // testConvert_addSqrtThenExp_samePrecedence_returnsOpWithExpThenSqrt - // testConvert_addTwoExps_lhs1Const_rhs1Const_lhs2Const_rhs2Const_returnsOpWithExp1Then2 - // ... parameterized: - // const^const const^const - // var^const const^const - // const^var const^const - // var^var const^const - // - // const^const var^const - // var^const var^const - // const^var var^const - // var^var var^const - // - // const^const const^var - // var^const const^var - // const^var const^var - // var^var const^var - // - // const^const var^var - // var^const var^var - // const^var var^var - // var^var var^var - // ... - // testConvert_addTwoSqrts_leftConst_rightConst_returnsOpWithSqrt1ThenSqrt2 - // testConvert_addTwoSqrts_leftVar_rightConst_returnsOpWithSqrt2ThenSqrt1 - // testConvert_addTwoSqrts_leftConst_rightVar_returnsOpWithSqrt1ThenSqrt2 - // testConvert_addTwoSqrts_leftVar_rightVar_returnsOpWithSqrt1ThenSqrt2 - - // testConvert_addTwoExps_lhs1Var_rhs1Const_lhs2Const_rhs2Const_returnsOpWithExp2Then1 - // testConvert_addTwoExps_lhs1Const_rhs1Var_lhs2Const_rhs2Const_returnsOpWithExp2Then1 - // testConvert_addTwoExps_lhs1Const_rhs1Var_lhs2Const_rhs2Var_returnsOpWithExp1Then2 - // testConvert_addTwoExps_lhs1Const_rhs1Var_lhs2Var_rhs2Const_returnsOpWithExp1Then2 - - /* Constant sorting */ - // testConvert_addTwoConstants_leftInteger2_rightInteger3_returnsOpWith2Then3 - // ... parameterized: - // left: 2 right: 3 - // left: 3 right: 2 - // - // left: 2 right: 3.14 - // left: 3.14 right: 2 - // - // left: 4 right: 3.14 - // left: 3.14 right: 4 - // - // left: 3.14 right: 6.28 - // left: 6.28 right: 3.14 - // ... - - /* Variable sorting */ - // testConvert_addTwoVariables_leftX_rightX_returnsOpBothXs - // testConvert_addTwoVariables_leftX_rightY_returnsOpWithXThenY - // ... parameterized: - // x, y; y, x; y, z; z, y; x, y, z; z, y, x - // ... + val comparable = expression.toComparableOperation() + + // Variables are sorted lexicographically. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(5) + index(0) { + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(3) { + variableTerm { + withNameThat().isEqualTo("y") + } + } + index(4) { + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + } /* Combined operations */ - // testConvert_allOperations_withNestedGroups_returnsCorrectlyStructuredAndOrderedOperation - - /* Equivalence checks */ - // testEquals_twoAdditionOps_differentByCommutativity_areEqual - // testEquals_twoAdditionOps_differentByAssociativity_areEqual - // testEquals_twoAdditionOps_differentByAssociativityAndCommutativity_areEqual - // testEquals_twoAdditionOps_differentByValue_areNotEqual - // testEquals_twoAdditionOps_differentByEvaluation_areNotEqual - // testEquals_twoMultOps_differentByCommutativity_areEqual - // testEquals_twoMultOps_differentByAssociativity_areEqual - // testEquals_twoMultOps_differentByAssociativityAndCommutativity_areEqual - // testEquals_twoMultOps_differentByValue_areNotEqual - // testEquals_twoMultOps_differentByEvaluation_areNotEqual - // TODO: for this & the next one, test with three operations (e.g. 2 / 3 / 4). - // testEquals_twoSubOps_same_areEqual - // testEquals_twoSubOps_differentByOrder_areNotEqual - // testEquals_twoSubOps_differentByValue_areNotEqual - // testEquals_twoDivOps_same_areEqual - // testEquals_twoDivOps_differentByOrder_areNotEqual - // testEquals_twoDivOps_differentByValue_areNotEqual - // testEquals_twoExps_same_areEqual - // testEquals_twoExps_differentByOrder_areNotEqual - // testEquals_twoExps_differentByValue_areNotEqual - // testEquals_twoSqrts_same_areEqual - // testEquals_twoSqrts_differentByValue_areNotEqual - // testEquals_twoOps_addsAndSubs_differentByOrder_areEqual - // testEquals_twoOps_multsAndDivs_differentByOrder_areEqual - // testEquals_twoOps_addsSubsMultsAndDivs_differentByOrder_areEqual - // testEquals_twoOps_allOperations_differentByOrder_areEqual - // testEquals_twoOps_allOperations_oneNestedDifferentByValue_areNotEqual - // testEquals_twoOps_allOperations_oneMissingTerm_areNotEqual + + @Test + fun testConvert_allOperations_withNestedGroups_returnsCorrectlyStructuredAndOrderedOperation() { + val expression = parseAlgebraicExpression("√(1+2*3)+-2^3*4/7-(2yx+x^(2+1)*(17/3))/-(x+(y+1.2))") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + // Sum of (ordered based on expected sorting criteria): + // - -(2yx+x^(2+1)*(17/3))/-(x+(y+1.2)) -> evaluates to positive + // - -2^3*4/7 + // - √(1+2*3) + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + // Product of (in sorted order): + // - 2yx+x^(2+1)*(17/3) + // - inverse of x+(y+1.2) + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + // Sum of (in sorted order): + // - x^(2+1)*(17/3) + // - 2yx + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + // Product of (in sorted order): + // - x^(2+1) + // - 17 + // - inverse of 3 + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + // Exponentiation of: x and 2+1. + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + // Summation of (in sorted order): 1 and 2. + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(17) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + // Product of (in sorted order): 2, x, and y. + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + // Sum of (in sorted order): 1.2, x, and y. + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIrrationalThat().isWithin(1e-5).of(1.2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + // Product of: + // - 2^3 + // - 4** + // - inverse of 7 + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + // Exponentiation of: 2 and 3. + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + squareRootWithArgument { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + // Sum of (in sorted order): + // - 2*3 + // - 1 + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + // Product of: 2 and 3. + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + } + } + } + } + } + + /* + * Equivalence checks. Note that these don't specifically verify doubles since they may not have + * reliable equivalence checking (and may instead require threshold checking for approximated + * equivalence). + * + * Further, these checks are using vanilla equivalence checking since they rely on the opreations + * being properly sorted. + */ + + @Test + fun testEquals_additionOps_differentByCommutativity_areEqual() { + val comparable1 = parseNumericExpression("1 + 2").toComparableOperation() + val comparable2 = parseNumericExpression("2 + 1").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_additionOps_differentByAssociativity_areEqual() { + val comparable1 = parseNumericExpression("1 + (2 + 3)").toComparableOperation() + val comparable2 = parseNumericExpression("(1 + 2) + 3").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_additionOps_differentByAssociativityAndCommutativity_areEqual() { + val comparable1 = parseNumericExpression("1 + (2 + 3)").toComparableOperation() + val comparable2 = parseNumericExpression("(2 + 1) + 3").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_additionOps_differentByValue_areNotEqual() { + val comparable1 = parseNumericExpression("1 + 2").toComparableOperation() + val comparable2 = parseNumericExpression("1 + 3").toComparableOperation() + + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_additionOps_sameOnlyByEvaluation_areNotEqual() { + val comparable1 = parseNumericExpression("1 + 2 + 2 + 1").toComparableOperation() + val comparable2 = parseNumericExpression("1 + 2 + 3").toComparableOperation() + + // While the two expressions are numerically equivalent, they aren't comparable since there are + // extra terms in one (more than trivial rearranging is required to determine that they're + // equal). + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_multiplicationOps_differentByCommutativity_areEqual() { + val comparable1 = parseNumericExpression("2 * 3").toComparableOperation() + val comparable2 = parseNumericExpression("3 * 2").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_multiplicationOps_differentByAssociativity_areEqual() { + val comparable1 = parseNumericExpression("2 * (3 * 4)").toComparableOperation() + val comparable2 = parseNumericExpression("(2 * 3) * 4").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_multiplicationOps_differentByAssociativityAndCommutativity_areEqual() { + val comparable1 = parseNumericExpression("2 * (3 * 4)").toComparableOperation() + val comparable2 = parseNumericExpression("(3 * 2) * 4").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_multiplicationOps_differentByValue_areNotEqual() { + val comparable1 = parseNumericExpression("2 * 3").toComparableOperation() + val comparable2 = parseNumericExpression("2 * 4").toComparableOperation() + + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_multiplicationOps_sameOnlyByEvaluation_areNotEqual() { + val comparable1 = parseNumericExpression("2 * 3 * 4").toComparableOperation() + val comparable2 = parseNumericExpression("2 * 2 * 2 * 3").toComparableOperation() + + // While the two expressions are numerically equivalent, they aren't comparable since there are + // extra terms in one (more than trivial rearranging is required to determine that they're + // equal). + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_subtractionOps_same_areEqual() { + val comparable1 = parseNumericExpression("1 - 2").toComparableOperation() + val comparable2 = parseNumericExpression("1 - 2").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_subtractionOps_differentByOrder_areNotEqual() { + val comparable1 = parseNumericExpression("1 - 2").toComparableOperation() + val comparable2 = parseNumericExpression("2 - 1").toComparableOperation() + + // Subtraction is not commutative. + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_subtractionOps_differentByAssociativity_areNotEqual() { + val comparable1 = parseNumericExpression("1 - (2 - 3)").toComparableOperation() + val comparable2 = parseNumericExpression("(1 - 2) - 3").toComparableOperation() + + // Subtraction is not associative. + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_subtractionOps_sameOnlyByEvaluation_areNotEqual() { + val comparable1 = parseNumericExpression("1 - 2 - 3").toComparableOperation() + val comparable2 = parseNumericExpression("1 - 2 - 2 - 1").toComparableOperation() + + // While the two expressions are numerically equivalent, they aren't comparable since there are + // extra terms in one (more than trivial rearranging is required to determine that they're + // equal). + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_divisionOps_same_areEqual() { + val comparable1 = parseNumericExpression("2 / 3").toComparableOperation() + val comparable2 = parseNumericExpression("2 / 3").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_divisionOps_differentByOrder_areNotEqual() { + val comparable1 = parseNumericExpression("2 / 3").toComparableOperation() + val comparable2 = parseNumericExpression("3 / 2").toComparableOperation() + + // Division is not commutative. + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_divisionOps_differentByAssociativity_areNotEqual() { + val comparable1 = parseNumericExpression("2 / (3 / 4)").toComparableOperation() + val comparable2 = parseNumericExpression("(2 / 3) / 4").toComparableOperation() + + // Division is not associative. + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_divisionOps_sameOnlyByEvaluation_areNotEqual() { + val comparable1 = parseNumericExpression("2 / 3 / 4").toComparableOperation() + val comparable2 = parseNumericExpression("2 / 3 / 2 / 2").toComparableOperation() + + // While the two expressions are numerically equivalent, they aren't comparable since there are + // extra terms in one (more than trivial rearranging is required to determine that they're + // equal). + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_exponentiationOps_same_areEqual() { + val comparable1 = parseNumericExpression("2 ^ 3").toComparableOperation() + val comparable2 = parseNumericExpression("2 ^ 3").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_exponentiationOps_differentByOrder_areNotEqual() { + val comparable1 = parseNumericExpression("2 ^ 3").toComparableOperation() + val comparable2 = parseNumericExpression("3 ^ 2").toComparableOperation() + + // Exponentiation is not commutative. + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_exponentiationOps_differentByAssociativity_areNotEqual() { + // Disable optional errors to allow nested exponentiation. + val comparable1 = + parseNumericExpression( + "2 ^ (3 ^ 4)", errorCheckingMode = REQUIRED_ONLY + ).toComparableOperation() + val comparable2 = + parseNumericExpression( + "(2 ^ 3) ^ 4", errorCheckingMode = REQUIRED_ONLY + ).toComparableOperation() + + // Exponentiation is not associative. + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_exponentiationOps_differentByValue_areNotEqual() { + val comparable1 = parseNumericExpression("2 ^ 3").toComparableOperation() + val comparable2 = parseNumericExpression("2 ^ 4").toComparableOperation() + + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_exponentiationOps_sameOnlyByEvaluation_areNotEqual() { + // Disable optional errors to allow nested exponentiation. + val comparable1 = parseNumericExpression("2 ^ 4").toComparableOperation() + val comparable2 = + parseNumericExpression("2 ^ 2 ^ 2", errorCheckingMode = REQUIRED_ONLY).toComparableOperation() + + // While the two expressions are numerically equivalent, they aren't comparable since there are + // extra terms in one (more than trivial rearranging is required to determine that they're + // equal). + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_squareRootOps_same_areEqual() { + val comparable1 = parseNumericExpression("sqrt(2)").toComparableOperation() + val comparable2 = parseNumericExpression("sqrt(2)").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_squareRootOps_differentByValue_areNotEqual() { + val comparable1 = parseNumericExpression("sqrt(2)").toComparableOperation() + val comparable2 = parseNumericExpression("sqrt(3)").toComparableOperation() + + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_squareRootOps_sameOnlyByEvaluation_areNotEqual() { + val comparable1 = parseNumericExpression("sqrt(2)").toComparableOperation() + val comparable2 = parseNumericExpression("sqrt(1 + 1)").toComparableOperation() + + // While the two expressions are numerically equivalent, they aren't comparable since there are + // extra terms in one (more than trivial rearranging is required to determine that they're + // equal). + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_additionsAndSubtractions_differentByOrder_areEqual() { + val comparable1 = parseNumericExpression("1+2-3").toComparableOperation() + val comparable2 = parseNumericExpression("-3+2+1").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_multiplicationsAndDivisions_differentByOrder_areEqual() { + val comparable1 = parseNumericExpression("2*3/4*7").toComparableOperation() + val comparable2 = parseNumericExpression("7*2*3/4").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_allAccumulationOperations_differentByOrder_areEqual() { + val comparable1 = parseNumericExpression("1+2*3/4*7-8+3").toComparableOperation() + val comparable2 = parseNumericExpression("-8+3+7*3/4*2+1").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_allOperations_differentByOrder_areEqual() { + val comparable1 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2*3)").toComparableOperation() + val comparable2 = parseNumericExpression("2^3*sqrt(3*2+1)/7-(-3*2+2)").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_allOperations_oneNestedDifferentByValue_areNotEqual() { + val comparable1 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2*3)").toComparableOperation() + val comparable2 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-4*3)").toComparableOperation() + + // Just one different term leads to the entire comparison failing. + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_twoOps_allOperations_oneMissingTerm_areNotEqual() { + val comparable1 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2*3)").toComparableOperation() + val comparable2 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2)").toComparableOperation() + + // Just one missing term leads to the entire comparison failing. + assertThat(comparable1).isNotEqualTo(comparable2) + } @Test fun test1() { @@ -1900,7 +3098,7 @@ class ExpressionToComparableOperationConverterTest { @Test fun test11() { // TODO: do something with this - val exp = parseNumericExpression("2^3^4", REQUIRED_ONLY) + val exp = parseNumericExpression("2^3^4", errorCheckingMode = REQUIRED_ONLY) assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() @@ -2702,7 +3900,7 @@ class ExpressionToComparableOperationConverterTest { @Test fun test31() { // TODO: do something with this - val exp = parseAlgebraicExpression("x^3^4", REQUIRED_ONLY) + val exp = parseAlgebraicExpression("x^3^4", errorCheckingMode = REQUIRED_ONLY) assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() @@ -3348,10 +4546,12 @@ class ExpressionToComparableOperationConverterTest { } private fun parseAlgebraicExpression( - expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + expression: String, + allowedVariables: List = listOf("x", "y", "z"), + errorCheckingMode: ErrorCheckingMode = ALL_ERRORS ): MathExpression { return MathExpressionParser.parseAlgebraicExpression( - expression, allowedVariables = listOf("x", "y", "z"), errorCheckingMode + expression, allowedVariables, errorCheckingMode ).retrieveExpectedSuccessfulResult() } From e6c09d85c095cbe0f44611f25787f5051b8556b8 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 28 Jan 2022 18:57:17 -0800 Subject: [PATCH 082/134] Simplify operation sorting comparators. --- .../org/oppia/android/util/math/BUILD.bazel | 3 + .../android/util/math/ComparatorExtensions.kt | 71 ++++------- ...xpressionToComparableOperationConverter.kt | 120 ++++++++---------- 3 files changed, 82 insertions(+), 112 deletions(-) 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 index d74e1683647..f203e85521d 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -91,6 +91,9 @@ kt_android_library( srcs = [ "ComparatorExtensions.kt", ], + deps = [ + "//third_party:com_google_protobuf_protobuf-javalite", + ], ) kt_android_library( diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt index 284ec69fe69..46ab552cfa2 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt @@ -1,57 +1,32 @@ package org.oppia.android.util.math -import java.util.SortedSet +import com.google.protobuf.MessageLite -fun comparingDeferred( - keySelector: (T) -> U, - comparatorSelector: () -> Comparator -): Comparator { - // Store as captured val for memoization. - val comparator by lazy { comparatorSelector() } - return Comparator.comparing(keySelector) { o1, o2 -> - comparator.compare(o1, o2) +fun Comparator.compareIterables(first: Iterable, second: Iterable): Int { + // Reference: https://stackoverflow.com/a/30107086. + val firstIter = first.sortedWith(this).iterator() + val secondIter = second.sortedWith(this).iterator() + while (firstIter.hasNext() && secondIter.hasNext()) { + val comparison = this.compare(firstIter.next(), secondIter.next()) + if (comparison != 0) return comparison // Found a different item. } -} - -fun > Comparator.thenComparingReversed( - keySelector: (T) -> U -): Comparator = thenComparing(Comparator.comparing(keySelector).reversed()) -fun > Comparator.thenSelectAmong( - enumSelector: (T) -> E, - vararg comparators: Pair> -): Comparator { - val comparatorMap = comparators.toMap() - return thenComparing( - Comparator { o1, o2 -> - val enum1 = enumSelector(o1) - val enum2 = enumSelector(o2) - check(enum1 == enum2) { - "Expected objects to have the same enum values: $o1 ($enum1), $o2 ($enum2)" - } - val comparator = - checkNotNull(comparatorMap[enum1]) { "No comparator for matched enum: $enum1" } - return@Comparator comparator.compare(o1, o2) - } - ) + // Everything is equal up to here, see if the lists are different length. + return when { + firstIter.hasNext() -> 1 // The first list is longer, therefore "greater." + secondIter.hasNext() -> -1 // Ditto, but for the second list. + else -> 0 // Otherwise, they're the same length with all equal items (and are thus equal). + } } -fun Comparator.toSetComparator(): Comparator> { - val itemComparator = this - return Comparator { first, second -> - // Reference: https://stackoverflow.com/a/30107086. - val firstIter = first.iterator() - val secondIter = second.iterator() - while (firstIter.hasNext() && secondIter.hasNext()) { - val comparison = itemComparator.compare(firstIter.next(), secondIter.next()) - if (comparison != 0) return@Comparator comparison // Found a different item. - } - - // Everything is equal up to here, see if the lists are different length. - return@Comparator when { - firstIter.hasNext() -> 1 // The first list is longer, therefore "greater." - secondIter.hasNext() -> -1 // Ditto, but for the second list. - else -> 0 // Otherwise, they're the same length with all equal items (and are thus equal). - } +fun Comparator.compareProtos(left: T, right: T): Int { + val defaultValue = left.defaultInstanceForType + val leftIsDefault = left == defaultValue + val rightIsDefault = right == defaultValue + return when { + leftIsDefault && rightIsDefault -> 0 // Both are default, therefore equal. + leftIsDefault -> 1 // right > left since it's initialized. + rightIsDefault -> 1 // left > right since it's initialized. + else -> compare(left, right) // Both are initialized; perform a deep-comparison. } } diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt index 5e9cc00b21f..4f1bc904aba 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt @@ -4,11 +4,8 @@ import org.oppia.android.app.model.ComparableOperation import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.PRODUCT import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.SUMMATION -import org.oppia.android.app.model.ComparableOperation.ComparisonTypeCase.CONSTANT_TERM -import org.oppia.android.app.model.ComparableOperation.ComparisonTypeCase.NON_COMMUTATIVE_OPERATION -import org.oppia.android.app.model.ComparableOperation.ComparisonTypeCase.VARIABLE_TERM import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation -import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation.OperationTypeCase +import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation.BinaryOperation import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE @@ -28,65 +25,10 @@ import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE -import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.invertNegation -import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.toComparableOperation class ExpressionToComparableOperationConverter private constructor() { companion object { - // TODO: consider eliminating the comparator extensions. Probably should verify full test suite - // & the old tests before deleting the old tests. - - private val COMPARABLE_OPERATION_COMPARATOR: Comparator by lazy { - // Some of the comparators must be deferred since they indirectly reference this comparator - // (which isn't valid until it's fully assembled). - Comparator.comparing(ComparableOperation::getComparisonTypeCase) - .thenComparing(ComparableOperation::getIsNegated) - .thenComparing(ComparableOperation::getIsInverted) - .thenSelectAmong( - ComparableOperation::getComparisonTypeCase, - ComparableOperation.ComparisonTypeCase.COMMUTATIVE_ACCUMULATION to comparingDeferred( - ComparableOperation::getCommutativeAccumulation - ) { COMMUTATIVE_ACCUMULATION_COMPARATOR }, - NON_COMMUTATIVE_OPERATION to comparingDeferred( - ComparableOperation::getNonCommutativeOperation - ) { NON_COMMUTATIVE_OPERATION_COMPARATOR }, - CONSTANT_TERM to Comparator.comparing( - ComparableOperation::getConstantTerm, REAL_COMPARATOR - ), - VARIABLE_TERM to Comparator.comparing(ComparableOperation::getVariableTerm) - ) - } - - private val COMMUTATIVE_ACCUMULATION_COMPARATOR: Comparator by lazy { - Comparator.comparing(CommutativeAccumulation::getAccumulationType) - .thenComparing( - { accumulation -> - accumulation.combinedOperationsList.toSortedSet(COMPARABLE_OPERATION_COMPARATOR) - }, - COMPARABLE_OPERATION_COMPARATOR.toSetComparator() - ) - } - - private val NON_COMMUTATIVE_BINARY_OPERATION_COMPARATOR by lazy { - Comparator.comparing( - NonCommutativeOperation.BinaryOperation::getLeftOperand, COMPARABLE_OPERATION_COMPARATOR - ).thenComparing( - NonCommutativeOperation.BinaryOperation::getRightOperand, COMPARABLE_OPERATION_COMPARATOR - ) - } - - private val NON_COMMUTATIVE_OPERATION_COMPARATOR: Comparator by lazy { - Comparator.comparing(NonCommutativeOperation::getOperationTypeCase) - .thenSelectAmong( - NonCommutativeOperation::getOperationTypeCase, - OperationTypeCase.EXPONENTIATION to Comparator.comparing( - NonCommutativeOperation::getExponentiation, NON_COMMUTATIVE_BINARY_OPERATION_COMPARATOR - ), - OperationTypeCase.SQUARE_ROOT to Comparator.comparing( - NonCommutativeOperation::getSquareRoot, COMPARABLE_OPERATION_COMPARATOR - ), - ) - } + private val COMPARABLE_OPERATION_COMPARATOR by lazy { createComparableOperationComparator() } fun MathExpression.toComparableOperation(): ComparableOperation { return when (expressionTypeCase) { @@ -244,20 +186,20 @@ class ExpressionToComparableOperationConverter private constructor() { // Replace the list operations with a sorted list of operations. Note that the inner elements // are already sorted since this is called during operation creation time (so nested // operations would have already been sorted). - val operationsList = combinedOperationsList.toMutableList() + val operationsList = combinedOperationsList.sortedWith(COMPARABLE_OPERATION_COMPARATOR) clearCombinedOperations() - addAllCombinedOperations(operationsList.sortedWith(COMPARABLE_OPERATION_COMPARATOR)) + addAllCombinedOperations(operationsList) } private fun MathExpression.toNonCommutativeOperation( setOperation: NonCommutativeOperation.Builder.( - NonCommutativeOperation.BinaryOperation + BinaryOperation ) -> NonCommutativeOperation.Builder ): ComparableOperation { return ComparableOperation.newBuilder().apply { nonCommutativeOperation = NonCommutativeOperation.newBuilder().apply { setOperation( - NonCommutativeOperation.BinaryOperation.newBuilder().apply { + BinaryOperation.newBuilder().apply { leftOperand = binaryOperation.leftOperand.toComparableOperation() rightOperand = binaryOperation.rightOperand.toComparableOperation() }.build() @@ -274,5 +216,55 @@ class ExpressionToComparableOperationConverter private constructor() { private fun ComparableOperation.invertInverted(): ComparableOperation = toBuilder().apply { isInverted = !isInverted }.build() + + private fun createComparableOperationComparator(): Comparator { + // Note that this & constituent comparators is designed to also verify undefined fields (such + // as all the possibilities of a oneof versus just one) for simpler syntax. Computationally, + // it shouldn't make a large difference since default protos are generally cached for proto + // lite, and compareProtos short-circuits for default protos. Further, a comparator is created + // for each staged of the execution since, unfortunately, there's no easy way to circularly + // reference cached fields. + return compareBy(ComparableOperation::getComparisonTypeCase) + .thenBy(ComparableOperation::getIsNegated) + .thenBy(ComparableOperation::getIsInverted) + .thenComparator { a, b -> + createCommutativeAccumulationComparator() + .compareProtos(a.commutativeAccumulation, b.commutativeAccumulation) + }.thenComparator { a, b -> + createNonCommutativeOperationComparator() + .compareProtos(a.nonCommutativeOperation, b.nonCommutativeOperation) + }.thenComparator { a, b -> + REAL_COMPARATOR.compareProtos(a.constantTerm, b.constantTerm) + } + .thenBy(ComparableOperation::getVariableTerm) + } + + private fun createCommutativeAccumulationComparator(): Comparator { + return compareBy(CommutativeAccumulation::getAccumulationType) + .thenComparator { a, b -> + createComparableOperationComparator().compareIterables( + a.combinedOperationsList, b.combinedOperationsList + ) + } + } + + private fun createNonCommutativeOperationComparator(): Comparator { + return compareBy(NonCommutativeOperation::getOperationTypeCase) + .thenComparator { a, b -> + createBinaryOperationComparator().compareProtos(a.exponentiation, b.exponentiation) + }.thenComparator { a, b -> + createComparableOperationComparator().compareProtos(a.squareRoot, b.squareRoot) + } + } + + private fun createBinaryOperationComparator(): Comparator { + // Start with a trivial comparator to start the chain for nicer syntax. + return compareBy(BinaryOperation::hasLeftOperand) + .thenComparator { a, b -> + createComparableOperationComparator().compareProtos(a.leftOperand, b.leftOperand) + }.thenComparator { a, b -> + createComparableOperationComparator().compareProtos(a.rightOperand, b.rightOperand) + } + } } } From bebc100bcc7f01d29bcb15944e7b77d267e8bfe8 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 28 Jan 2022 18:58:14 -0800 Subject: [PATCH 083/134] Remove old tests. --- ...ssionToComparableOperationConverterTest.kt | 1802 ----------------- 1 file changed, 1802 deletions(-) diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt index 5d4dc8495fe..925ac470db8 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt @@ -2734,1808 +2734,6 @@ class ExpressionToComparableOperationConverterTest { assertThat(comparable1).isNotEqualTo(comparable2) } - @Test - fun test1() { - // TODO: do something with this - val exp = parseNumericExpression("1") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - } - - @Test - fun test2() { - // TODO: do something with this - val exp = parseNumericExpression("-1") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - } - - @Test - fun test3() { - // TODO: do something with this - val exp = parseNumericExpression("1+3+4") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - - @Test - fun test4() { - // TODO: do something with this - val exp = parseNumericExpression("-1-2-3") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(2) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - - @Test - fun test5() { - // TODO: do something with this - val exp = parseNumericExpression("1+2-3") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(2) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - - @Test - fun test6() { - // TODO: do something with this - val exp = parseNumericExpression("2*3*4") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - - @Test - fun test7() { - // TODO: do something with this - val exp = parseNumericExpression("1-2*3") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - } - } - } - - @Test - fun test8() { - // TODO: do something with this - val exp = parseNumericExpression("2*3-4") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - index(1) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - - @Test - fun test9() { - // TODO: do something with this - val exp = parseNumericExpression("1+2*3-4+8*7*6-9") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(5) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(6) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(8) - } - } - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(3) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - index(4) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(9) - } - } - } - } - } - - @Test - fun test10() { - // TODO: do something with this - val exp = parseNumericExpression("2/3/4") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - - @Test - fun test11() { - // TODO: do something with this - val exp = parseNumericExpression("2^3^4", errorCheckingMode = REQUIRED_ONLY) - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - } - } - - @Test - fun test12() { - // TODO: do something with this - val exp = parseNumericExpression("1+2/3+3") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - - @Test - fun test13() { - // TODO: do something with this - val exp = parseNumericExpression("1+(2/3)+3") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - - @Test - fun test14() { - // TODO: do something with this - val exp = parseNumericExpression("1+2^3+3") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - - @Test - fun test15() { - // TODO: do something with this - val exp = parseNumericExpression("1+(2^3)+3") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - - @Test - fun test16() { - // TODO: do something with this - val exp = parseNumericExpression("2*3/4*7") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(4) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - - @Test - fun test17() { - // TODO: do something with this - val exp = parseNumericExpression("2*(3/4)*7") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(4) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - - @Test - fun test18() { - // TODO: do something with this - val exp = parseNumericExpression("-3*sqrt(2)") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - squareRootWithArgument { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - - @Test - fun test19() { - // TODO: do something with this - val exp = parseNumericExpression("1+(2+(3+(4+5)))") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(5) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - index(4) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - } - } - } - - @Test - fun test20() { - // TODO: do something with this - val exp = parseNumericExpression("2*(3*(4*(5*6)))") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(5) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - index(4) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(6) - } - } - } - } - } - - @Test - fun test21() { - // TODO: do something with this - val exp = parseAlgebraicExpression("x") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - } - - @Test - fun test22() { - // TODO: do something with this - val exp = parseAlgebraicExpression("-x") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - } - - @Test - fun test23() { - // TODO: do something with this - val exp = parseAlgebraicExpression("1+x+y") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - } - - @Test - fun test24() { - // TODO: do something with this - val exp = parseAlgebraicExpression("-1-x-y") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - } - - @Test - fun test25() { - // TODO: do something with this - val exp = parseAlgebraicExpression("1+x-y") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - } - - @Test - fun test26() { - // TODO: do something with this - val exp = parseAlgebraicExpression("2xy") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - } - - @Test - fun test27() { - // TODO: do something with this - val exp = parseAlgebraicExpression("1-xy") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - } - } - } - - @Test - fun test28() { - // TODO: do something with this - val exp = parseAlgebraicExpression("xy-4") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - index(1) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - - @Test - fun test29() { - // TODO: do something with this - val exp = parseAlgebraicExpression("1+xy-4+yz-9") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(5) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(3) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - index(4) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(9) - } - } - } - } - } - - @Test - fun test30() { - // TODO: do something with this - val exp = parseAlgebraicExpression("2/x/y") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - } - - @Test - fun test31() { - // TODO: do something with this - val exp = parseAlgebraicExpression("x^3^4", errorCheckingMode = REQUIRED_ONLY) - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - } - } - - @Test - fun test32() { - // TODO: do something with this - val exp = parseAlgebraicExpression("1+x/y+z") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - } - } - } - - @Test - fun test33() { - // TODO: do something with this - val exp = parseAlgebraicExpression("1+(x/y)+z") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - } - } - } - - @Test - fun test34() { - // TODO: do something with this - val exp = parseAlgebraicExpression("1+x^3+y") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - } - - @Test - fun test35() { - // TODO: do something with this - val exp = parseAlgebraicExpression("1+(x^3)+y") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - } - - @Test - fun test36() { - // TODO: do something with this - val exp = parseAlgebraicExpression("2*x/y*z") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(4) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - } - - @Test - fun test37() { - // TODO: do something with this - val exp = parseAlgebraicExpression("2*(x/y)*z") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(4) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - } - - @Test - fun test38() { - // TODO: do something with this - val exp = parseAlgebraicExpression("-2*sqrt(x)") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - squareRootWithArgument { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - - @Test - fun test39() { - // TODO: do something with this - val exp = parseAlgebraicExpression("1+(x+(3+(z+y)))") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(5) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - index(4) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - } - } - } - - @Test - fun test40() { - // TODO: do something with this - val exp = parseAlgebraicExpression("2*(x*(4*(zy)))") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(5) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - index(4) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - } - } - } - - // TODO: Equality tests: - @Test - fun test41() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("(1+2)+3") - val secondList = createComparableOperationListFromNumericExpression("1+(2+3)") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test42() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("1+2+3") - val secondList = createComparableOperationListFromNumericExpression("3+2+1") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test43() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("1-2-3") - val secondList = createComparableOperationListFromNumericExpression("-3 + -2 + 1") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test44() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("1-2-3") - val secondList = createComparableOperationListFromNumericExpression("-3-2+1") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test45() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("1-2-3") - val secondList = createComparableOperationListFromNumericExpression("-3-2+1") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test46() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("1-2-3") - val secondList = createComparableOperationListFromNumericExpression("3-2-1") - assertThat(firstList).isNotEqualTo(secondList) - } - - @Test - fun test47() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("2*3*4") - val secondList = createComparableOperationListFromNumericExpression("4*3*2") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test48() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("2*(3/4)") - val secondList = createComparableOperationListFromNumericExpression("3/4*2") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test49() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("2*3/4") - val secondList = createComparableOperationListFromNumericExpression("3/4*2") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test50() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("2*3/4") - val secondList = createComparableOperationListFromNumericExpression("2*3*4") - assertThat(firstList).isNotEqualTo(secondList) - } - - @Test - fun test51() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("2*3/4") - val secondList = createComparableOperationListFromNumericExpression("2*4/3") - assertThat(firstList).isNotEqualTo(secondList) - } - - @Test - fun test52() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("2*3/4*7") - val secondList = createComparableOperationListFromNumericExpression("3/4*7*2") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test53() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("2*3/4*7") - val secondList = createComparableOperationListFromNumericExpression("7*(3*2/4)") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test54() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("2*3/4*7") - val secondList = createComparableOperationListFromNumericExpression("7*3*2/4") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test55() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("-2*3") - val secondList = createComparableOperationListFromNumericExpression("3*-2") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test56() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("2^3") - val secondList = createComparableOperationListFromNumericExpression("3^2") - assertThat(firstList).isNotEqualTo(secondList) - } - - @Test - fun test57() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("-(1+2)") - val secondList = createComparableOperationListFromNumericExpression("-1+2") - assertThat(firstList).isNotEqualTo(secondList) - } - - @Test - fun test58() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("-(1+2)") - val secondList = createComparableOperationListFromNumericExpression("-1-2") - assertThat(firstList).isNotEqualTo(secondList) - } - - @Test - fun test59() { - // TODO: do something with this - val firstList = createComparableOperationListFromAlgebraicExpression("x(x+1)") - val secondList = createComparableOperationListFromAlgebraicExpression("(1+x)x") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test60() { - // TODO: do something with this - val firstList = createComparableOperationListFromAlgebraicExpression("x(x+1)") - val secondList = createComparableOperationListFromAlgebraicExpression("x^2+x") - assertThat(firstList).isNotEqualTo(secondList) - } - - @Test - fun test61() { - // TODO: do something with this - val firstList = createComparableOperationListFromAlgebraicExpression("x^2*sqrt(x)") - val secondList = createComparableOperationListFromAlgebraicExpression("x") - assertThat(firstList).isNotEqualTo(secondList) - } - - @Test - fun test62() { - // TODO: do something with this - val firstList = createComparableOperationListFromAlgebraicExpression("xyz") - val secondList = createComparableOperationListFromAlgebraicExpression("zyx") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test63() { - // TODO: do something with this - val firstList = createComparableOperationListFromAlgebraicExpression("1+xy-2") - val secondList = createComparableOperationListFromAlgebraicExpression("-2+1+yx") - assertThat(firstList).isEqualTo(secondList) - } - - private fun createComparableOperationListFromNumericExpression(expression: String) = - parseNumericExpression(expression).toComparableOperation() - - private fun createComparableOperationListFromAlgebraicExpression(expression: String) = - parseAlgebraicExpression(expression).toComparableOperation() - private companion object { private fun parseNumericExpression( expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS From 3cecc6cc35efc5d2e7c88c3cf4d497eeb16ad8cb Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 28 Jan 2022 20:31:39 -0800 Subject: [PATCH 084/134] Add remaining missing tests. --- .../android/util/math/ComparatorExtensions.kt | 2 +- .../util/math/MathExpressionExtensions.kt | 2 +- .../oppia/android/util/math/RealExtensions.kt | 1 - .../org/oppia/android/util/math/BUILD.bazel | 1 + .../util/math/ComparatorExtensionsTest.kt | 269 +++++++++++++++++- .../util/math/MathExpressionExtensionsTest.kt | 25 +- .../android/util/math/RealExtensionsTest.kt | 207 +++++++++++++- 7 files changed, 499 insertions(+), 8 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt index 46ab552cfa2..99b66e11243 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt @@ -25,7 +25,7 @@ fun Comparator.compareProtos(left: T, right: T): Int { val rightIsDefault = right == defaultValue return when { leftIsDefault && rightIsDefault -> 0 // Both are default, therefore equal. - leftIsDefault -> 1 // right > left since it's initialized. + leftIsDefault -> -1 // right > left since it's initialized. rightIsDefault -> 1 // left > right since it's initialized. else -> compare(left, right) // Both are initialized; perform a deep-comparison. } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 07b3d949fee..109490e5abe 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -31,4 +31,4 @@ fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(div */ fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() -fun MathExpression.toComparableOperation(): ComparableOperation = toComparableOperation() +fun MathExpression.toComparable(): ComparableOperation = toComparableOperation() diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index ef0e21a6bcd..0006df1e5d4 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -9,7 +9,6 @@ import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET import kotlin.math.absoluteValue import kotlin.math.pow -// TODO: add tests. val REAL_COMPARATOR: Comparator by lazy { Comparator.comparing(Real::toDouble) } /** 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 index a6fe0270cea..8a1f784aef3 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -49,6 +49,7 @@ oppia_android_test( test_class = "org.oppia.android.util.math.ComparatorExtensionsTest", test_manifest = "//utility:test_manifest", deps = [ + "//model/src/main/proto:test_models", "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_truth", "//third_party:junit_junit", diff --git a/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt index bb27e759bb2..91c8f352e4a 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt @@ -1,8 +1,10 @@ package org.oppia.android.util.math 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.app.model.TestMessage import org.robolectric.annotation.LooperMode /** Tests for [Comparator] extensions. */ @@ -11,10 +13,271 @@ import org.robolectric.annotation.LooperMode @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class ComparatorExtensionsTest { - // TODO: finish tests + private companion object { + private val TEST_MESSAGE_0 = TestMessage.newBuilder().apply { intValue = 0 }.build() + private val TEST_MESSAGE_1 = TestMessage.newBuilder().apply { intValue = 1 }.build() + private val TEST_MESSAGE_2 = TestMessage.newBuilder().apply { intValue = 2 }.build() + } + + private val stringComparator: Comparator by lazy { + Comparator { o1, o2 -> o1.compareTo(o2) } + } + private val protoComparator: Comparator by lazy { + compareBy(TestMessage::getIntValue) + } + + @Test + fun testCompareIterables_emptyList_emptyList_returnsZero() { + val leftList = listOf() + val rightList = listOf() + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + assertThat(compareResult).isEqualTo(0) + } + + @Test + fun testCompareIterables_singletonList_emptyList_returnsOne() { + val leftList = listOf("1") + val rightList = listOf() + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + assertThat(compareResult).isEqualTo(1) + } @Test - fun test() { - throw Exception() + fun testCompareIterables_emptyList_singletonList_returnsNegativeOne() { + val leftList = listOf() + val rightList = listOf("1") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterables_singletonList_singletonList_sameElems_returnsZero() { + val leftList = listOf("1") + val rightList = listOf("1") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + assertThat(compareResult).isEqualTo(0) + } + + @Test + fun testCompareIterables_twoItemList_singletonList_commonElem_returnsOne() { + val leftList = listOf("1", "2") + val rightList = listOf("1") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + // The first list is larger, therefore "greater". + assertThat(compareResult).isEqualTo(1) + } + + @Test + fun testCompareIterables_singletonList_twoItemList_commonElem_returnsNegativeOne() { + val leftList = listOf("1") + val rightList = listOf("1", "2") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterables_equalSizeLists_sameItems_sameOrder_returnsZero() { + val leftList = listOf("1", "2") + val rightList = listOf("1", "2") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + assertThat(compareResult).isEqualTo(0) + } + + @Test + fun testCompareIterables_equalSizeLists_sameItems_differentOrder_returnsZero() { + val leftList = listOf("1", "2") + val rightList = listOf("2", "1") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + // Order shouldn't matter. + assertThat(compareResult).isEqualTo(0) + } + + @Test + fun testCompareIterables_list223_list123_returnsOne() { + val leftList = listOf("2", "2", "3") + val rightList = listOf("1", "2", "3") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + // The first element is larger in the left list, so it's "greater". + assertThat(compareResult).isEqualTo(1) + } + + @Test + fun testCompareIterables_list123_list223_returnsNegativeOne() { + val leftList = listOf("1", "2", "3") + val rightList = listOf("2", "2", "3") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterables_list123_list11_returnsOne() { + val leftList = listOf("1", "2", "3") + val rightList = listOf("1", "1") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + // The second item is bigger in the first list, so it's "greater". + assertThat(compareResult).isEqualTo(1) + } + + @Test + fun testCompareIterables_list123_list13_returnsNegativeOne() { + val leftList = listOf("1", "2", "3") + val rightList = listOf("1", "3") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + // The second item is bigger in the second list, so the first one is "lesser". + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterables_list223_list1_returnsOne() { + val leftList = listOf("1", "2", "3") + val rightList = listOf("1") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + assertThat(compareResult).isEqualTo(1) + } + + @Test + fun testCompareIterables_list123_list2_returnsNegativeOne() { + val leftList = listOf("1", "2", "3") + val rightList = listOf("2") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterables_list22_list2_returnsOne() { + val leftList = listOf("2", "2") + val rightList = listOf("2") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + // The first list has an extra element. + assertThat(compareResult).isEqualTo(1) + } + + @Test + fun testCompareIterables_list2_list22_returnsNegativeOne() { + val leftList = listOf("2") + val rightList = listOf("2", "2") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + // The second list has an extra element. + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterables_list22_list22_returnsZero() { + val leftList = listOf("2", "2") + val rightList = listOf("2", "2") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + assertThat(compareResult).isEqualTo(0) + } + + @Test + fun testCompareProtos_defaultAndDefault_returnsZero() { + val leftProto = TestMessage.newBuilder().build() + val rightProto = TestMessage.newBuilder().build() + + val compareResult = protoComparator.compareProtos(leftProto, rightProto) + + // Two default instances are equal. + assertThat(compareResult).isEqualTo(0) + } + + @Test + fun testCompareProtos_nonDefaultZeroAndDefault_returnsZero() { + val leftProto = TEST_MESSAGE_0 + val rightProto = TestMessage.newBuilder().build() + + val compareResult = protoComparator.compareProtos(leftProto, rightProto) + + // Even though the left proto is defined, the value of 0 for its int field makes it the same as + // a default (per proto3 spec). + assertThat(compareResult).isEqualTo(0) + } + + @Test + fun testCompareProtos_nonDefaultAndDefault_returnsOne() { + val leftProto = TEST_MESSAGE_1 + val rightProto = TestMessage.newBuilder().build() + + val compareResult = protoComparator.compareProtos(leftProto, rightProto) + + // The left proto is actually defined. + assertThat(compareResult).isEqualTo(1) + } + + @Test + fun testCompareProtos_defaultAndNonDefault_returnsNegativeOne() { + val leftProto = TestMessage.newBuilder().build() + val rightProto = TEST_MESSAGE_1 + + val compareResult = protoComparator.compareProtos(leftProto, rightProto) + + // The right proto is actually defined. + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareProtos_twoNonDefaults_sameProtoValues_returnsZero() { + val leftProto = TEST_MESSAGE_1 + val rightProto = TEST_MESSAGE_1 + + val compareResult = protoComparator.compareProtos(leftProto, rightProto) + + // The protos are equal per protoComparator. + assertThat(compareResult).isEqualTo(0) + } + + @Test + fun testCompareProtos_twoNonDefaults_leftIsLarger_returnsOne() { + val leftProto = TEST_MESSAGE_2 + val rightProto = TEST_MESSAGE_1 + + val compareResult = protoComparator.compareProtos(leftProto, rightProto) + + // The left proto is larger per protoComparator. + assertThat(compareResult).isEqualTo(1) + } + + @Test + fun testCompareProtos_twoNonDefaults_leftIsSmaller_returnsNegativeOne() { + val leftProto = TEST_MESSAGE_1 + val rightProto = TEST_MESSAGE_2 + + val compareResult = protoComparator.compareProtos(leftProto, rightProto) + + // The right proto is larger per protoComparator. + assertThat(compareResult).isEqualTo(-1) } } diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt index 0a81df14c54..586a7253c9d 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt @@ -19,7 +19,8 @@ import org.robolectric.annotation.LooperMode * Note that this suite only verifies that the extensions work at a high-level. More specific * verifications for operations like LaTeX conversion and expression evaluation are part of more * targeted test suites such as [ExpressionToLatexConverterTest] and - * [NumericExpressionEvaluatorTest]. + * [NumericExpressionEvaluatorTest]. For comparable operations, see + * [ExpressionToComparableOperationConverterTest]. */ // FunctionName: test names are conventionally named with underscores. // SameParameterValue: tests should have specific context included/excluded for readability. @@ -72,6 +73,28 @@ class MathExpressionExtensionsTest { assertThat(result).isIrrationalThat().isWithin(1e-5).of(322194.700361352) } + @Test + fun testToComparableOperation_twoAlgebraicExpressions_differentOrders_returnsEqualOperations() { + val expression1 = parseAlgebraicExpression("x+2/x-(-7*8-9)+sqrt((x+2)+1)+3xy^2") + val expression2 = parseAlgebraicExpression("sqrt(x+(1+2))+2/x+x-(-9+8*-7-3y^2x)") + + val operation1 = expression1.toComparable() + val operation2 = expression2.toComparable() + + assertThat(operation1).isEqualTo(operation2) + } + + @Test + fun testToComparableOperation_twoAlgebraicExpressions_differentValue_returnsUnequalOperations() { + val expression1 = parseAlgebraicExpression("x+2/x-(-7*8-9)+sqrt((x+2)+1)+3xy^2") + val expression2 = parseAlgebraicExpression("sqrt(x+(1+3))+2/x+x-(-9+8*-7-3y^2x)") + + val operation1 = expression1.toComparable() + val operation2 = expression2.toComparable() + + assertThat(operation1).isNotEqualTo(operation2) + } + private companion object { private fun parseNumericExpression(expression: String): MathExpression { return parseNumericExpression(expression, ALL_ERRORS).retrieveExpectedSuccessfulResult() diff --git a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt index 9760b175ff8..43234d63feb 100644 --- a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt @@ -13,7 +13,15 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameter import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.robolectric.annotation.LooperMode -/** Tests for [Real] extensions. */ +/** + * Tests for [Real] extensions. + * + * Note that this suite makes special use of parameterized tests to significantly reduce the length + * of the suite, even partially at the expensive of good testing practices (since many of the + * parameterized tests are actually verifying multiple behaviors). Given the generally trivial + * nature of these behaviors, this trade-off is considered acceptable. That being said, this pattern + * should only be replicated elsewhere in the codebase after thorough consideration. + */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @RunWith(OppiaParameterizedTestRunner::class) @@ -1774,6 +1782,203 @@ class RealExtensionsTest { assertThat(result).isIrrationalThat().isWithin(1e-5).of(1.772004515) } + /* + * Tests for REAL_COMPARATOR. + * + * Note that these specifically don't try to compare against negative doubles since the comparison + * logic is a bit unexpected (see https://stackoverflow.com/a/45544483). + */ + + @Test + @RunParameterized( + Iteration("0==0", "lhsInt=0", "rhsInt=0", "expInt=0"), + Iteration("-2<0", "lhsInt=-2", "rhsInt=0", "expInt=-1"), + Iteration("-5<-2", "lhsInt=-5", "rhsInt=-2", "expInt=-1"), + Iteration("-2>-5", "lhsInt=-2", "rhsInt=-5", "expInt=1"), + Iteration("2>0", "lhsInt=2", "rhsInt=0", "expInt=1"), + Iteration("5>2", "lhsInt=5", "rhsInt=2", "expInt=1"), + Iteration("2<5", "lhsInt=2", "rhsInt=5", "expInt=-1"), + Iteration("-2<5", "lhsInt=-2", "rhsInt=5", "expInt=-1"), + Iteration("5>-2", "lhsInt=5", "rhsInt=-2", "expInt=1") + ) + fun testComparator_intAndInt_returnsCorrectComparisonInt() { + val lhsValue = createIntegerReal(lhsInt) + val rhsValue = createIntegerReal(rhsInt) + + val comparison = REAL_COMPARATOR.compare(lhsValue, rhsValue) + + assertThat(comparison).isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("0==0", "lhsInt=0", "rhsFrac=0", "expInt=0"), + Iteration("-2<0", "lhsInt=-2", "rhsFrac=0", "expInt=-1"), + Iteration("-5<-2", "lhsInt=-5", "rhsFrac=-2", "expInt=-1"), + Iteration("-5<-1/2", "lhsInt=-5", "rhsFrac=-1/2", "expInt=-1"), + Iteration("-2>-5", "lhsInt=-2", "rhsFrac=-5", "expInt=1"), + Iteration("-1>-3/2", "lhsInt=-1", "rhsFrac=-3/2", "expInt=1"), + Iteration("2>0", "lhsInt=2", "rhsFrac=0", "expInt=1"), + Iteration("5>2", "lhsInt=5", "rhsFrac=2", "expInt=1"), + Iteration("2<5", "lhsInt=2", "rhsFrac=5", "expInt=-1"), + Iteration("2<7/2", "lhsInt=2", "rhsFrac=7/2", "expInt=-1"), + Iteration("5>3/2", "lhsInt=5", "rhsFrac=3/2", "expInt=1"), + Iteration("-2<5", "lhsInt=-2", "rhsFrac=5", "expInt=-1"), + Iteration("-2<3/2", "lhsInt=-2", "rhsFrac=3/2", "expInt=-1"), + Iteration("5>-2", "lhsInt=5", "rhsFrac=-2", "expInt=1"), + Iteration("5>-3/2", "lhsInt=5", "rhsFrac=-3/2", "expInt=1") + ) + fun testComparator_intAndFraction_returnsCorrectComparisonInt() { + val lhsValue = createIntegerReal(lhsInt) + val rhsValue = createRationalReal(rhsFrac) + + val comparison = REAL_COMPARATOR.compare(lhsValue, rhsValue) + + assertThat(comparison).isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("0==0", "lhsInt=0", "rhsDouble=0.0", "expInt=0"), + Iteration("-2<0", "lhsInt=-2", "rhsDouble=0.0", "expInt=-1"), + Iteration("-5<-3.14", "lhsInt=-5", "rhsDouble=-3.14", "expInt=-1"), + Iteration("-2>-6.28", "lhsInt=-2", "rhsDouble=-6.28", "expInt=1"), + Iteration("2>0", "lhsInt=2", "rhsDouble=0.0", "expInt=1"), + Iteration("5>3.14", "lhsInt=5", "rhsDouble=3.14", "expInt=1"), + Iteration("2<6.28", "lhsInt=2", "rhsDouble=6.28", "expInt=-1"), + Iteration("-2<3.14", "lhsInt=-2", "rhsDouble=3.14", "expInt=-1"), + Iteration("2>-3.14", "lhsInt=2", "rhsDouble=-3.14", "expInt=1") + ) + fun testComparator_intAndDouble_returnsCorrectComparisonInt() { + val lhsValue = createIntegerReal(lhsInt) + val rhsValue = createIrrationalReal(rhsDouble) + + val comparison = REAL_COMPARATOR.compare(lhsValue, rhsValue) + + assertThat(comparison).isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("0==0", "lhsFrac=0", "rhsInt=0", "expInt=0"), + Iteration("-3/2<0", "lhsFrac=-3/2", "rhsInt=0", "expInt=-1"), + Iteration("-7/2<-3", "lhsFrac=-7/2", "rhsInt=-3", "expInt=-1"), + Iteration("-3/2>-5", "lhsFrac=-3/2", "rhsInt=-5", "expInt=1"), + Iteration("3/2>0", "lhsFrac=3/2", "rhsInt=0", "expInt=1"), + Iteration("7/2>3", "lhsFrac=7/2", "rhsInt=3", "expInt=1"), + Iteration("3/2<5", "lhsFrac=3/2", "rhsInt=5", "expInt=-1"), + Iteration("-3/2<3", "lhsFrac=-3/2", "rhsInt=3", "expInt=-1"), + Iteration("3/2>-3", "lhsFrac=3/2", "rhsInt=-3", "expInt=1") + ) + fun testComparator_fractionAndInt_returnsCorrectComparisonInt() { + val lhsValue = createRationalReal(lhsFrac) + val rhsValue = createIntegerReal(rhsInt) + + val comparison = REAL_COMPARATOR.compare(lhsValue, rhsValue) + + assertThat(comparison).isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("0==0", "lhsFrac=0", "rhsFrac=0", "expInt=0"), + Iteration("-3/2<0", "lhsFrac=-3/2", "rhsFrac=0", "expInt=-1"), + Iteration("-7/2<-3/2", "lhsFrac=-7/2", "rhsFrac=-3/2", "expInt=-1"), + Iteration("3/2>0", "lhsFrac=3/2", "rhsFrac=0", "expInt=1"), + Iteration("7/2>3/2", "lhsFrac=7/2", "rhsFrac=3/2", "expInt=1"), + Iteration("3/2<7/2", "lhsFrac=3/2", "rhsFrac=7/2", "expInt=-1"), + Iteration("-3/2<3/2", "lhsFrac=-3/2", "rhsFrac=3/2", "expInt=-1"), + Iteration("3/2>-3/2", "lhsFrac=3/2", "rhsFrac=-3/2", "expInt=1") + ) + fun testComparator_fractionAndFraction_returnsCorrectComparisonInt() { + val lhsValue = createRationalReal(lhsFrac) + val rhsValue = createRationalReal(rhsFrac) + + val comparison = REAL_COMPARATOR.compare(lhsValue, rhsValue) + + assertThat(comparison).isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("0==0", "lhsFrac=0", "rhsDouble=0.0", "expInt=0"), + Iteration("-3/2<0", "lhsFrac=-3/2", "rhsDouble=0.0", "expInt=-1"), + Iteration("-7/2<-3.14", "lhsFrac=-7/2", "rhsDouble=-3.14", "expInt=-1"), + Iteration("3/2>0", "lhsFrac=3/2", "rhsDouble=0.0", "expInt=1"), + Iteration("7/2>3.14", "lhsFrac=7/2", "rhsDouble=3.14", "expInt=1"), + Iteration("3/2<3.14", "lhsFrac=3/2", "rhsDouble=3.14", "expInt=-1"), + Iteration("-3/2<3.14", "lhsFrac=-3/2", "rhsDouble=3.14", "expInt=-1"), + Iteration("3/2>-3.14", "lhsFrac=3/2", "rhsDouble=-3.14", "expInt=1") + ) + fun testComparator_fractionAndDouble_returnsCorrectComparisonInt() { + val lhsValue = createRationalReal(lhsFrac) + val rhsValue = createIrrationalReal(rhsDouble) + + val comparison = REAL_COMPARATOR.compare(lhsValue, rhsValue) + + assertThat(comparison).isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("0==0", "lhsDouble=0.0", "rhsInt=0", "expInt=0"), + Iteration("-3.14<0", "lhsDouble=-3.14", "rhsInt=0", "expInt=-1"), + Iteration("-6.28<-4", "lhsDouble=-6.28", "rhsInt=-4", "expInt=-1"), + Iteration("3.14>0", "lhsDouble=3.14", "rhsInt=0", "expInt=1"), + Iteration("6.28>4", "lhsDouble=6.28", "rhsInt=4", "expInt=1"), + Iteration("3.14<4", "lhsDouble=3.14", "rhsInt=4", "expInt=-1"), + Iteration("-3.14<4", "lhsDouble=-3.14", "rhsInt=4", "expInt=-1"), + Iteration("3.14>-4", "lhsDouble=3.14", "rhsInt=-4", "expInt=1") + ) + fun testComparator_doubleAndInt_returnsCorrectComparisonInt() { + val lhsValue = createIrrationalReal(lhsDouble) + val rhsValue = createIntegerReal(rhsInt) + + val comparison = REAL_COMPARATOR.compare(lhsValue, rhsValue) + + assertThat(comparison).isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("0==0", "lhsDouble=0.0", "rhsFrac=0", "expInt=0"), + Iteration("-3.14<0", "lhsDouble=-3.14", "rhsFrac=0", "expInt=-1"), + Iteration("-6.28<-7/2", "lhsDouble=-6.28", "rhsFrac=-7/2", "expInt=-1"), + Iteration("3.14>0", "lhsDouble=3.14", "rhsFrac=0", "expInt=1"), + Iteration("6.28>7/2", "lhsDouble=6.28", "rhsFrac=7/2", "expInt=1"), + Iteration("3.14<7/2", "lhsDouble=3.14", "rhsFrac=7/2", "expInt=-1"), + Iteration("-3.14<7/2", "lhsDouble=-3.14", "rhsFrac=7/2", "expInt=-1"), + Iteration("3.14>-7/2", "lhsDouble=3.14", "rhsFrac=-7/2", "expInt=1") + ) + fun testComparator_doubleAndFraction_returnsCorrectComparisonInt() { + val lhsValue = createIrrationalReal(lhsDouble) + val rhsValue = createRationalReal(rhsFrac) + + val comparison = REAL_COMPARATOR.compare(lhsValue, rhsValue) + + assertThat(comparison).isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("0==0", "lhsDouble=0.0", "rhsDouble=0.0", "expInt=0"), + Iteration("-3.14<0", "lhsDouble=-3.14", "rhsDouble=0.0", "expInt=-1"), + Iteration("-6.28<-3.14", "lhsDouble=-6.28", "rhsDouble=-3.14", "expInt=-1"), + Iteration("3.14>0", "lhsDouble=3.14", "rhsDouble=0.0", "expInt=1"), + Iteration("6.28>3.14", "lhsDouble=6.28", "rhsDouble=3.14", "expInt=1"), + Iteration("3.14<6.28", "lhsDouble=3.14", "rhsDouble=6.28", "expInt=-1"), + Iteration("-3.14<6.28", "lhsDouble=-3.14", "rhsDouble=6.28", "expInt=-1"), + Iteration("3.14>-6.28", "lhsDouble=3.14", "rhsDouble=-6.28", "expInt=1") + ) + fun testComparator_doubleAndDouble_returnsCorrectComparisonInt() { + val lhsValue = createIrrationalReal(lhsDouble) + val rhsValue = createIrrationalReal(rhsDouble) + + val comparison = REAL_COMPARATOR.compare(lhsValue, rhsValue) + + assertThat(comparison).isEqualTo(expInt) + } + private fun createRationalReal(rawFractionExpression: String) = createRationalReal(fractionParser.parseFractionFromString(rawFractionExpression)) } From 573fee9deada59fd7d13db2a004b7562642f40f0 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 28 Jan 2022 20:45:31 -0800 Subject: [PATCH 085/134] KDocs & test exemption. --- .../file_content_validation_checks.textproto | 1 + .../android/util/math/ComparatorExtensions.kt | 20 ++ ...xpressionToComparableOperationConverter.kt | 40 ++- .../util/math/MathExpressionExtensions.kt | 9 +- .../oppia/android/util/math/RealExtensions.kt | 5 + .../org/oppia/android/util/math/BUILD.bazel | 4 +- .../util/math/ComparatorExtensionsTest.kt | 3 +- ...ssionToComparableOperationConverterTest.kt | 268 +++++++++--------- .../util/math/MathExpressionExtensionsTest.kt | 8 +- 9 files changed, 205 insertions(+), 153 deletions(-) diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index 8b8fe90e4cd..bcae6e30b40 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -287,6 +287,7 @@ file_content_checks { prohibited_content_regex: "OppiaParameterizedTestRunner" failure_message: "To use OppiaParameterizedTestRunner, please add an exemption to file_content_validation_checks.textproto and add an explanation for your use case in your PR description. Note that parameterized tests should only be used in special circumstances where a single behavior can be tested across multiple inputs, or for especially large test suites that can be trivially reduced." exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" + exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt" diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt index 99b66e11243..4e4355d0ec0 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt @@ -2,6 +2,17 @@ package org.oppia.android.util.math import com.google.protobuf.MessageLite +/** + * Compares two [Iterable]s based on an item [Comparator] and returns the result. + * + * The two [Iterable]s are iterated in an order determined by [Comparator], and then compared + * element-by-element. If any element is different, the difference of that item becomes the overall + * difference. If the lists are different sizes (but otherwise match up to the boundary of one + * list), then the longer list will be considered "greater". + * + * This means that two [Iterable]s are only equal if they have the same number of elements, and that + * all of their items are equal per this [Comparator], including duplicates. + */ fun Comparator.compareIterables(first: Iterable, second: Iterable): Int { // Reference: https://stackoverflow.com/a/30107086. val firstIter = first.sortedWith(this).iterator() @@ -19,6 +30,15 @@ fun Comparator.compareIterables(first: Iterable, second: Iterable): } } +/** + * Compares two protos of type [T] ([left] and [right]) using this [Comparator] and returns the + * result. + * + * This adds behavior above the standard ``compare`` function by short-circuiting if either proto is + * equal to the default instance (in which case "defined" is always considered larger, and this + * [Comparator] isn't used). This short-circuiting behavior can be useful when comparing recursively + * infinite proto structures to avoid stack overflows.. + */ fun Comparator.compareProtos(left: T, right: T): Int { val defaultValue = left.defaultInstanceForType val leftIsDefault = left == defaultValue diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt index 4f1bc904aba..5983f09e27d 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt @@ -26,11 +26,31 @@ import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +/** + * Converter from [MathExpression] to [ComparableOperation]. + * + * See the separate proto details for context, and [convertToComparableOperation] for the actual conversion + * function. + */ class ExpressionToComparableOperationConverter private constructor() { companion object { private val COMPARABLE_OPERATION_COMPARATOR by lazy { createComparableOperationComparator() } - fun MathExpression.toComparableOperation(): ComparableOperation { + /** + * Returns a new [ComparableOperation] representing this [MathExpression]. + * + * Comparable operations are representations of math expressions that are deterministically + * arranged to ensure two expressions that only differ due to associativity or commutativity are + * still equal. This is done by combining neighboring arithmetic operations into accumulations, + * and still retaining the structures for non-commutative operations. The order of all + * operations is well-defined and deterministic. Further, how elements retain inverted or + * negated properties is also deterministic (and designed to minimize negative values). + * + * The tests for this method provide very thorough and broad examples of different cases that + * this function supports. In particular, the equality tests are useful to see what sorts of + * expressions can be considered the same per [ComparableOperation]. + */ + fun MathExpression.convertToComparableOperation(): ComparableOperation { return when (expressionTypeCase) { CONSTANT -> ComparableOperation.newBuilder().apply { constantTerm = constant @@ -49,21 +69,21 @@ class ExpressionToComparableOperationConverter private constructor() { ComparableOperation.getDefaultInstance() } UNARY_OPERATION -> when (unaryOperation.operator) { - NEGATE -> unaryOperation.operand.toComparableOperation().invertNegation() - POSITIVE -> unaryOperation.operand.toComparableOperation() + NEGATE -> unaryOperation.operand.convertToComparableOperation().invertNegation() + POSITIVE -> unaryOperation.operand.convertToComparableOperation() UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> ComparableOperation.getDefaultInstance() } FUNCTION_CALL -> when (functionCall.functionType) { SQUARE_ROOT -> ComparableOperation.newBuilder().apply { nonCommutativeOperation = NonCommutativeOperation.newBuilder().apply { - squareRoot = functionCall.argument.toComparableOperation() + squareRoot = functionCall.argument.convertToComparableOperation() }.build() }.build() FunctionType.FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> ComparableOperation.getDefaultInstance() } - GROUP -> group.toComparableOperation() + GROUP -> group.convertToComparableOperation() EXPRESSIONTYPE_NOT_SET, null -> ComparableOperation.getDefaultInstance() } } @@ -119,8 +139,8 @@ class ExpressionToComparableOperationConverter private constructor() { addOperationToSum(expression.unaryOperation.operand, forceNegative) // Skip groups so that nested operations can be properly combined. expression.expressionTypeCase == GROUP -> addOperationToSum(expression.group, forceNegative) - forceNegative -> addCombinedOperations(expression.toComparableOperation().invertNegation()) - else -> addCombinedOperations(expression.toComparableOperation()) + forceNegative -> addCombinedOperations(expression.convertToComparableOperation().invertNegation()) + else -> addCombinedOperations(expression.convertToComparableOperation()) } } @@ -169,7 +189,7 @@ class ExpressionToComparableOperationConverter private constructor() { expression.expressionTypeCase == GROUP -> addOperationToProduct(expression.group, forceInverse, invertNegation) else -> { - val operationExpression = expression.toComparableOperation() + val operationExpression = expression.convertToComparableOperation() val potentiallyInvertedExpression = if (invertNegation) { operationExpression.invertNegation() } else operationExpression @@ -200,8 +220,8 @@ class ExpressionToComparableOperationConverter private constructor() { nonCommutativeOperation = NonCommutativeOperation.newBuilder().apply { setOperation( BinaryOperation.newBuilder().apply { - leftOperand = binaryOperation.leftOperand.toComparableOperation() - rightOperand = binaryOperation.rightOperand.toComparableOperation() + leftOperand = binaryOperation.leftOperand.convertToComparableOperation() + rightOperand = binaryOperation.rightOperand.convertToComparableOperation() }.build() ) }.build() diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 109490e5abe..6b89e83acdb 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -4,7 +4,7 @@ import org.oppia.android.app.model.ComparableOperation import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression import org.oppia.android.app.model.Real -import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.toComparableOperation +import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.convertToComparableOperation import org.oppia.android.util.math.ExpressionToLatexConverter.Companion.convertToLatex import org.oppia.android.util.math.NumericExpressionEvaluator.Companion.evaluate @@ -31,4 +31,9 @@ fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(div */ fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() -fun MathExpression.toComparable(): ComparableOperation = toComparableOperation() +/** + * Returns the [ComparableOperation] representation of this [MathExpression]. + * + * See [convertToComparableOperation] for details. + */ +fun MathExpression.toComparableOperation(): ComparableOperation = convertToComparableOperation() diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 0006df1e5d4..69f4ab19aa2 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -9,6 +9,11 @@ import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET import kotlin.math.absoluteValue import kotlin.math.pow +/** + * [Comparator] for [Real]s that ensures two reals can be compared even if they are different types. + * + * Note that no reliance should be placed on how negative zeros for doubles and fractions behave. + */ val REAL_COMPARATOR: Comparator by lazy { Comparator.comparing(Real::toDouble) } /** 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 index 8a1f784aef3..9c4a4ef43bb 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -67,12 +67,12 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", "//testing/src/main/java/org/oppia/android/testing/math:comparable_operation_subject", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", - "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", - "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", "//third_party:robolectric_android-all", "//utility/src/main/java/org/oppia/android/util/math:extensions", "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", diff --git a/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt index 91c8f352e4a..5c44c17968e 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt @@ -178,7 +178,8 @@ class ComparatorExtensionsTest { val compareResult = stringComparator.compareIterables(leftList, rightList) - // The first list has an extra element. + // The first list has an extra element. This also verifies that duplicates are correctly + // considered during comparison. assertThat(compareResult).isEqualTo(1) } diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt index 925ac470db8..1452a9c70bd 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt @@ -2,7 +2,7 @@ package org.oppia.android.util.math import com.google.common.truth.Truth.assertThat import org.junit.Test -import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.toComparableOperation +import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.convertToComparableOperation import org.junit.runner.RunWith import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.PRODUCT import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.SUMMATION @@ -38,7 +38,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_integerConstantExpression_returnsConstantOperation() { val expression = parseNumericExpression("2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { constantTerm { @@ -51,7 +51,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_decimalConstantExpression_returnsConstantOperation() { val expression = parseNumericExpression("3.14") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { constantTerm { @@ -64,7 +64,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_variableExpression_returnsVariableOperation() { val expression = parseAlgebraicExpression("x") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { variableTerm { @@ -77,7 +77,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addition_returnsSummation() { val expression = parseNumericExpression("1+2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { commutativeAccumulationWithType(SUMMATION) { @@ -102,7 +102,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addition_sameValues_returnsSummationWithBoth() { val expression = parseNumericExpression("1+1") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { commutativeAccumulationWithType(SUMMATION) { @@ -127,7 +127,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_subtraction_returnsSummationOfNegative() { val expression = parseNumericExpression("1-2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { commutativeAccumulationWithType(SUMMATION) { @@ -152,7 +152,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplication_returnsProduct() { val expression = parseNumericExpression("2*3") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { commutativeAccumulationWithType(PRODUCT) { @@ -176,7 +176,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_division_returnsProductOfInverted() { val expression = parseNumericExpression("2/3") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { commutativeAccumulationWithType(PRODUCT) { @@ -200,7 +200,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_exponentiation_returnsNonCommutativeOperation() { val expression = parseNumericExpression("2^3") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { nonCommutativeOperation { @@ -224,7 +224,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_squareRoot_returnsNonCommutativeOperation() { val expression = parseNumericExpression("sqrt(2)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { nonCommutativeOperation { @@ -241,7 +241,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_variableTerm_returnsNonNegativeOperation() { val expression = parseAlgebraicExpression("x") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() @@ -252,7 +252,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_negatedVariable_returnsNegativeVariableOperation() { val expression = parseAlgebraicExpression("-x") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasNegatedPropertyThat().isTrue() @@ -266,7 +266,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_positiveVariable_returnsVariableOperation() { val expression = parseAlgebraicExpression("+x", errorCheckingMode = REQUIRED_ONLY) - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() @@ -280,7 +280,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_positiveOfNegativeVariable_returnsNegativeVariableOperation() { val expression = parseAlgebraicExpression("+-x", errorCheckingMode = REQUIRED_ONLY) - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasNegatedPropertyThat().isTrue() @@ -295,7 +295,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_subtractionOfNegative_returnsSummationWithPositives() { val expression = parseNumericExpression("1--2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Verify that the subtraction & negation cancel out each other. assertThat(comparable).hasStructureThatMatches { @@ -322,7 +322,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_negativePlusPositive_returnsSummationWithFirstTermNegative() { val expression = parseNumericExpression("-2+1") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Verify that the negative only applies to the 2, not to the whole expression. assertThat(comparable).hasStructureThatMatches { @@ -349,7 +349,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multipleAdditions_returnsCombinedSummation() { val expression = parseNumericExpression("1+2+3") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() @@ -381,7 +381,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multipleSubtractions_returnsCombinedSummation() { val expression = parseNumericExpression("1-2-3") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() @@ -413,7 +413,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_additionsAndSubtractions_returnsCombinedSummation() { val expression = parseNumericExpression("1+2-3-4+5") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() @@ -457,7 +457,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_additionsWithNestedAdds_returnsCompletelyCombinedSummation() { val expression = parseNumericExpression("1+((2+(3+4)+5)+6)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() @@ -507,7 +507,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_subtractsWithNesting_returnsSummationWithDistributedNegation() { val expression = parseNumericExpression("1-(2+3-4)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Both the 2 & 3 are negative since the subtraction distributes, and the 4 becomes positive. assertThat(comparable).hasStructureThatMatches { @@ -546,7 +546,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_subtractsWithNestedSubs_returnsCompletelyCombinedSummation() { val expression = parseNumericExpression("1-((2-(3-4)-5)-6)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Some of these are positive because of distribution. assertThat(comparable).hasStructureThatMatches { @@ -598,7 +598,7 @@ class ExpressionToComparableOperationConverterTest { val expression = parseNumericExpression("1++(2-3)+-(4+5--(2+3-1))", errorCheckingMode = REQUIRED_ONLY) - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // This also verifies that negation distributes in the same way as subtraction. assertThat(comparable).hasStructureThatMatches { @@ -661,7 +661,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multipleMultiplications_returnsCombinedProduct() { val expression = parseNumericExpression("2*3*4") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasInvertedPropertyThat().isFalse() @@ -693,7 +693,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multipleDivisions_returnsCombinedProduct() { val expression = parseNumericExpression("2/3/4") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasInvertedPropertyThat().isFalse() @@ -725,7 +725,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplicationsAndDivisions_returnsCombinedProduct() { val expression = parseNumericExpression("2*3/4/5*6") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasInvertedPropertyThat().isFalse() @@ -769,7 +769,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplicationsWithNestedMults_returnsCompletelyCombinedProduct() { val expression = parseNumericExpression("2*((3*(4*5)*6)*7)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasInvertedPropertyThat().isFalse() @@ -819,7 +819,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_dividesWithNesting_returnsProductWithDistributedInversion() { val expression = parseNumericExpression("2/(3*4/5)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Both the 3 & 5 become inverted, and the 5 becomes regular multiplication due to the division // distribution. @@ -860,7 +860,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_divisionsWithNestedDivs_returnsCompletelyCombinedProduct() { val expression = parseNumericExpression("2/((3/(4/5)/6)/7)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Some of these are non-inverted because of distribution. assertThat(comparable).hasStructureThatMatches { @@ -911,7 +911,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplicationsAndDivisionsWithNested_returnsCombinedProduct() { val expression = parseNumericExpression("1*(2/3)/(4*5*(2*3/1))") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasInvertedPropertyThat().isFalse() @@ -973,7 +973,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplicationWithNoNegatives_returnsPositiveProduct() { val expression = parseNumericExpression("2*3") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() @@ -996,7 +996,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplicationWithOneNegative_returnsNegativeProduct() { val expression = parseNumericExpression("2*-3") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // The entire accumulation is considered negative. assertThat(comparable).hasStructureThatMatches { @@ -1026,7 +1026,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplicationWithTwoNegatives_returnsPositiveProduct() { val expression = parseNumericExpression("-2*-3") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // The two negatives cancel out. This also verifies that negation can pipe up to top-level // negation. @@ -1057,7 +1057,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplicationWithThreeNegatives_returnsNegativeProduct() { val expression = parseNumericExpression("-2*-3*-4") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // 3 negative operands results in the overall product being negative. assertThat(comparable).hasStructureThatMatches { @@ -1094,7 +1094,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_combinedMultDivWithNested_evenNegatives_returnsPositiveProduct() { val expression = parseNumericExpression("-2*-3/-(4/-(3*2))") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // There are four negatives, so the overall expression is positive. assertThat(comparable).hasStructureThatMatches { @@ -1108,7 +1108,7 @@ class ExpressionToComparableOperationConverterTest { val expression = parseNumericExpression("-2*-3/-(4/-(3*2*+(-3*7)))", errorCheckingMode = REQUIRED_ONLY) - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // There are five negatives, so the overall expression is negative. Note that this is also // verifying that the negation properly distributes with the group. @@ -1127,7 +1127,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_additionAndExp_returnsSummationWithNonCommutative() { val expression = parseNumericExpression("1+2^3") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { commutativeAccumulationWithType(SUMMATION) { @@ -1161,7 +1161,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_additionAndSquareRoot_returnsSummationWithNonCommutative() { val expression = parseNumericExpression("1+sqrt(2)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { commutativeAccumulationWithType(SUMMATION) { @@ -1188,7 +1188,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_additionWithinExp_returnsSummationWithinNonCommutative() { val expression = parseNumericExpression("2^(1+3)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { nonCommutativeOperation { @@ -1222,7 +1222,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_additionWithinSquareRoot_returnsSummationWithinNonCommutative() { val expression = parseNumericExpression("sqrt(1+3)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { nonCommutativeOperation { @@ -1249,7 +1249,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplicationAndExp_returnsProductWithNonCommutative() { val expression = parseNumericExpression("2*3^4") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { commutativeAccumulationWithType(PRODUCT) { @@ -1283,7 +1283,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplicationAndSquareRoot_returnsProductWithNonCommutative() { val expression = parseNumericExpression("2*sqrt(3)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { commutativeAccumulationWithType(PRODUCT) { @@ -1310,7 +1310,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplicationWithinExp_returnsProductWithinNonCommutative() { val expression = parseNumericExpression("2^(3*4)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { nonCommutativeOperation { @@ -1344,7 +1344,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplicationWithinSquareRoot_returnsProductWithinNonCommutative() { val expression = parseNumericExpression("sqrt(2*3)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { nonCommutativeOperation { @@ -1371,7 +1371,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_additionAndMultiplication_returnsSummationOfProduct() { val expression = parseNumericExpression("2*3+1") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { commutativeAccumulationWithType(SUMMATION) { @@ -1404,7 +1404,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplicationAndGroupedAddition_returnsProductOfSummation() { val expression = parseNumericExpression("2*(3+1)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { commutativeAccumulationWithType(PRODUCT) { @@ -1454,7 +1454,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_additionAndNonCommutativeOp_samePrecedence_returnsOpWithSummationFirst() { val expression = parseNumericExpression("$op1 * $op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Verify that the summation is still first since it's higher priority during sorting. assertThat(comparable).hasStructureThatMatches { @@ -1480,7 +1480,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_constantAndNonCommutativeOp_samePrecedence_returnsOpWithNonCommutativeFirst() { val expression = parseNumericExpression("$op1 + $op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Verify that the non-commutative operation is first since it's higher priority during sorting. assertThat(comparable).hasStructureThatMatches { @@ -1504,7 +1504,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_constantAndVariable_samePrecedence_returnsOpWithConstantFirst() { val expression = parseAlgebraicExpression("$op1 * $op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Verify that the constant is first since it's higher priority during sorting. assertThat(comparable).hasStructureThatMatches { @@ -1528,7 +1528,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_positiveAndNegativeVariables_returnsOpWithNegatedLast() { val expression = parseAlgebraicExpression("$op1 + $op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Verify that the positive term is first since it's higher priority during sorting. assertThat(comparable).hasStructureThatMatches { @@ -1554,7 +1554,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_invertedAndNonInvertedVariables_returnsOpWithInvertedLast() { val expression = parseAlgebraicExpression("$op1 * $op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Verify that the non-inverted term is first since it's higher priority during sorting. assertThat(comparable).hasStructureThatMatches { @@ -1590,7 +1590,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_twoAdditionsInProduct_smallerSumIsFirst() { val expression = parseNumericExpression("($op1)*($op2)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Summations are deterministically sorted regardless of how the original expression structures // them. @@ -1635,7 +1635,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_twoMultiplicationsInSum_smallerProductIsFirst() { val expression = parseNumericExpression("($op1)+($op2)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Products are deterministically sorted regardless of how the original expression structures // them. @@ -1676,7 +1676,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_expAndSqrt_samePrecedence_returnsOpWithExpThenSqrt() { val expression = parseNumericExpression("$op1+$op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Verify that the exponentiation is first since it's higher priority during sorting. assertThat(comparable).hasStructureThatMatches { @@ -1719,7 +1719,7 @@ class ExpressionToComparableOperationConverterTest { val expression = parseAlgebraicExpression("($op1)+($op2)", errorCheckingMode = REQUIRED_ONLY) - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Verify that the exponentiations are ordered based on the left-hand operand's size. assertThat(comparable).hasStructureThatMatches { @@ -1778,7 +1778,7 @@ class ExpressionToComparableOperationConverterTest { errorCheckingMode = REQUIRED_ONLY ) - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Verify that the exponentiations are ordered based on the left-hand operand's lexicographical // ordering. @@ -1819,7 +1819,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addTwoSqrts_leftConst_rightConst_returnsOpWithSqrtsByArgSize() { val expression = parseNumericExpression("sqrt($op1)+sqrt($op2)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // The square roots should be ordered based on their argument sorting. assertThat(comparable).hasStructureThatMatches { @@ -1855,7 +1855,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addTwoSqrts_leftVar_rightVar_returnsOpWithSqrtsByVariableOrder() { val expression = parseAlgebraicExpression("sqrt($op1)+sqrt($op2)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // The square roots should be ordered based on their argument lexicographical sorting. assertThat(comparable).hasStructureThatMatches { @@ -1891,7 +1891,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addTwoSqrts_oneConst_oneVar_returnsOpWithSqrtsByConstFirst() { val expression = parseAlgebraicExpression("sqrt($op1)+sqrt($op2)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Constant-before-variable ordering also affects peer square root orders. assertThat(comparable).hasStructureThatMatches { @@ -1929,7 +1929,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addTwoConstants_leftInteger_rightInteger_returnsOpSortedByValues() { val expression = parseNumericExpression("$op1 + $op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // The order of the summation should be based on the constants' values. assertThat(comparable).hasStructureThatMatches { @@ -1957,7 +1957,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addTwoConstants_leftDouble_rightDouble_returnsOpSortedByValues() { val expression = parseNumericExpression("$op1 + $op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // The order of the summation should be based on the constants' values. assertThat(comparable).hasStructureThatMatches { @@ -1985,7 +1985,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addTwoConstants_smallInt_largeDouble_returnsOpWithIntFirst() { val expression = parseNumericExpression("$op1 + $op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // The order of the summation should be based on the constants' values. assertThat(comparable).hasStructureThatMatches { @@ -2013,7 +2013,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addTwoConstants_largeInt_smallDouble_returnsOpWithDoubleFirst() { val expression = parseNumericExpression("$op1 + $op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // The order of the summation should be based on the constants' values. assertThat(comparable).hasStructureThatMatches { @@ -2041,7 +2041,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addVarAndIntConstant_returnsOpWithConstantFirst() { val expression = parseAlgebraicExpression("$op1 + $op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Constants are always ordered before variables. assertThat(comparable).hasStructureThatMatches { @@ -2069,7 +2069,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addVarAndDoubleConstant_returnsOpWithConstantFirst() { val expression = parseAlgebraicExpression("$op1 + $op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // The order of the summation should be based on the constants' values. assertThat(comparable).hasStructureThatMatches { @@ -2093,7 +2093,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addTwoVariables_leftX_rightX_returnsOpBothXs() { val expression = parseAlgebraicExpression("x + x") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { commutativeAccumulationWithType(SUMMATION) { @@ -2120,7 +2120,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addTwoVariables_oneX_oneY_returnsOpWithXThenY() { val expression = parseAlgebraicExpression("$op1 + $op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Variables are sorted lexicographically. assertThat(comparable).hasStructureThatMatches { @@ -2144,7 +2144,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addMultipleVars_returnsOpWithThemInOrder() { val expression = parseAlgebraicExpression("x + z + x + y + x") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Variables are sorted lexicographically. assertThat(comparable).hasStructureThatMatches { @@ -2185,7 +2185,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_allOperations_withNestedGroups_returnsCorrectlyStructuredAndOrderedOperation() { val expression = parseAlgebraicExpression("√(1+2*3)+-2^3*4/7-(2yx+x^(2+1)*(17/3))/-(x+(y+1.2))") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() @@ -2445,40 +2445,40 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_additionOps_differentByCommutativity_areEqual() { - val comparable1 = parseNumericExpression("1 + 2").toComparableOperation() - val comparable2 = parseNumericExpression("2 + 1").toComparableOperation() + val comparable1 = parseNumericExpression("1 + 2").convertToComparableOperation() + val comparable2 = parseNumericExpression("2 + 1").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_additionOps_differentByAssociativity_areEqual() { - val comparable1 = parseNumericExpression("1 + (2 + 3)").toComparableOperation() - val comparable2 = parseNumericExpression("(1 + 2) + 3").toComparableOperation() + val comparable1 = parseNumericExpression("1 + (2 + 3)").convertToComparableOperation() + val comparable2 = parseNumericExpression("(1 + 2) + 3").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_additionOps_differentByAssociativityAndCommutativity_areEqual() { - val comparable1 = parseNumericExpression("1 + (2 + 3)").toComparableOperation() - val comparable2 = parseNumericExpression("(2 + 1) + 3").toComparableOperation() + val comparable1 = parseNumericExpression("1 + (2 + 3)").convertToComparableOperation() + val comparable2 = parseNumericExpression("(2 + 1) + 3").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_additionOps_differentByValue_areNotEqual() { - val comparable1 = parseNumericExpression("1 + 2").toComparableOperation() - val comparable2 = parseNumericExpression("1 + 3").toComparableOperation() + val comparable1 = parseNumericExpression("1 + 2").convertToComparableOperation() + val comparable2 = parseNumericExpression("1 + 3").convertToComparableOperation() assertThat(comparable1).isNotEqualTo(comparable2) } @Test fun testEquals_additionOps_sameOnlyByEvaluation_areNotEqual() { - val comparable1 = parseNumericExpression("1 + 2 + 2 + 1").toComparableOperation() - val comparable2 = parseNumericExpression("1 + 2 + 3").toComparableOperation() + val comparable1 = parseNumericExpression("1 + 2 + 2 + 1").convertToComparableOperation() + val comparable2 = parseNumericExpression("1 + 2 + 3").convertToComparableOperation() // While the two expressions are numerically equivalent, they aren't comparable since there are // extra terms in one (more than trivial rearranging is required to determine that they're @@ -2488,40 +2488,40 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_multiplicationOps_differentByCommutativity_areEqual() { - val comparable1 = parseNumericExpression("2 * 3").toComparableOperation() - val comparable2 = parseNumericExpression("3 * 2").toComparableOperation() + val comparable1 = parseNumericExpression("2 * 3").convertToComparableOperation() + val comparable2 = parseNumericExpression("3 * 2").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_multiplicationOps_differentByAssociativity_areEqual() { - val comparable1 = parseNumericExpression("2 * (3 * 4)").toComparableOperation() - val comparable2 = parseNumericExpression("(2 * 3) * 4").toComparableOperation() + val comparable1 = parseNumericExpression("2 * (3 * 4)").convertToComparableOperation() + val comparable2 = parseNumericExpression("(2 * 3) * 4").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_multiplicationOps_differentByAssociativityAndCommutativity_areEqual() { - val comparable1 = parseNumericExpression("2 * (3 * 4)").toComparableOperation() - val comparable2 = parseNumericExpression("(3 * 2) * 4").toComparableOperation() + val comparable1 = parseNumericExpression("2 * (3 * 4)").convertToComparableOperation() + val comparable2 = parseNumericExpression("(3 * 2) * 4").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_multiplicationOps_differentByValue_areNotEqual() { - val comparable1 = parseNumericExpression("2 * 3").toComparableOperation() - val comparable2 = parseNumericExpression("2 * 4").toComparableOperation() + val comparable1 = parseNumericExpression("2 * 3").convertToComparableOperation() + val comparable2 = parseNumericExpression("2 * 4").convertToComparableOperation() assertThat(comparable1).isNotEqualTo(comparable2) } @Test fun testEquals_multiplicationOps_sameOnlyByEvaluation_areNotEqual() { - val comparable1 = parseNumericExpression("2 * 3 * 4").toComparableOperation() - val comparable2 = parseNumericExpression("2 * 2 * 2 * 3").toComparableOperation() + val comparable1 = parseNumericExpression("2 * 3 * 4").convertToComparableOperation() + val comparable2 = parseNumericExpression("2 * 2 * 2 * 3").convertToComparableOperation() // While the two expressions are numerically equivalent, they aren't comparable since there are // extra terms in one (more than trivial rearranging is required to determine that they're @@ -2531,16 +2531,16 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_subtractionOps_same_areEqual() { - val comparable1 = parseNumericExpression("1 - 2").toComparableOperation() - val comparable2 = parseNumericExpression("1 - 2").toComparableOperation() + val comparable1 = parseNumericExpression("1 - 2").convertToComparableOperation() + val comparable2 = parseNumericExpression("1 - 2").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_subtractionOps_differentByOrder_areNotEqual() { - val comparable1 = parseNumericExpression("1 - 2").toComparableOperation() - val comparable2 = parseNumericExpression("2 - 1").toComparableOperation() + val comparable1 = parseNumericExpression("1 - 2").convertToComparableOperation() + val comparable2 = parseNumericExpression("2 - 1").convertToComparableOperation() // Subtraction is not commutative. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2548,8 +2548,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_subtractionOps_differentByAssociativity_areNotEqual() { - val comparable1 = parseNumericExpression("1 - (2 - 3)").toComparableOperation() - val comparable2 = parseNumericExpression("(1 - 2) - 3").toComparableOperation() + val comparable1 = parseNumericExpression("1 - (2 - 3)").convertToComparableOperation() + val comparable2 = parseNumericExpression("(1 - 2) - 3").convertToComparableOperation() // Subtraction is not associative. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2557,8 +2557,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_subtractionOps_sameOnlyByEvaluation_areNotEqual() { - val comparable1 = parseNumericExpression("1 - 2 - 3").toComparableOperation() - val comparable2 = parseNumericExpression("1 - 2 - 2 - 1").toComparableOperation() + val comparable1 = parseNumericExpression("1 - 2 - 3").convertToComparableOperation() + val comparable2 = parseNumericExpression("1 - 2 - 2 - 1").convertToComparableOperation() // While the two expressions are numerically equivalent, they aren't comparable since there are // extra terms in one (more than trivial rearranging is required to determine that they're @@ -2568,16 +2568,16 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_divisionOps_same_areEqual() { - val comparable1 = parseNumericExpression("2 / 3").toComparableOperation() - val comparable2 = parseNumericExpression("2 / 3").toComparableOperation() + val comparable1 = parseNumericExpression("2 / 3").convertToComparableOperation() + val comparable2 = parseNumericExpression("2 / 3").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_divisionOps_differentByOrder_areNotEqual() { - val comparable1 = parseNumericExpression("2 / 3").toComparableOperation() - val comparable2 = parseNumericExpression("3 / 2").toComparableOperation() + val comparable1 = parseNumericExpression("2 / 3").convertToComparableOperation() + val comparable2 = parseNumericExpression("3 / 2").convertToComparableOperation() // Division is not commutative. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2585,8 +2585,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_divisionOps_differentByAssociativity_areNotEqual() { - val comparable1 = parseNumericExpression("2 / (3 / 4)").toComparableOperation() - val comparable2 = parseNumericExpression("(2 / 3) / 4").toComparableOperation() + val comparable1 = parseNumericExpression("2 / (3 / 4)").convertToComparableOperation() + val comparable2 = parseNumericExpression("(2 / 3) / 4").convertToComparableOperation() // Division is not associative. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2594,8 +2594,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_divisionOps_sameOnlyByEvaluation_areNotEqual() { - val comparable1 = parseNumericExpression("2 / 3 / 4").toComparableOperation() - val comparable2 = parseNumericExpression("2 / 3 / 2 / 2").toComparableOperation() + val comparable1 = parseNumericExpression("2 / 3 / 4").convertToComparableOperation() + val comparable2 = parseNumericExpression("2 / 3 / 2 / 2").convertToComparableOperation() // While the two expressions are numerically equivalent, they aren't comparable since there are // extra terms in one (more than trivial rearranging is required to determine that they're @@ -2605,16 +2605,16 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_exponentiationOps_same_areEqual() { - val comparable1 = parseNumericExpression("2 ^ 3").toComparableOperation() - val comparable2 = parseNumericExpression("2 ^ 3").toComparableOperation() + val comparable1 = parseNumericExpression("2 ^ 3").convertToComparableOperation() + val comparable2 = parseNumericExpression("2 ^ 3").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_exponentiationOps_differentByOrder_areNotEqual() { - val comparable1 = parseNumericExpression("2 ^ 3").toComparableOperation() - val comparable2 = parseNumericExpression("3 ^ 2").toComparableOperation() + val comparable1 = parseNumericExpression("2 ^ 3").convertToComparableOperation() + val comparable2 = parseNumericExpression("3 ^ 2").convertToComparableOperation() // Exponentiation is not commutative. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2626,11 +2626,11 @@ class ExpressionToComparableOperationConverterTest { val comparable1 = parseNumericExpression( "2 ^ (3 ^ 4)", errorCheckingMode = REQUIRED_ONLY - ).toComparableOperation() + ).convertToComparableOperation() val comparable2 = parseNumericExpression( "(2 ^ 3) ^ 4", errorCheckingMode = REQUIRED_ONLY - ).toComparableOperation() + ).convertToComparableOperation() // Exponentiation is not associative. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2638,8 +2638,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_exponentiationOps_differentByValue_areNotEqual() { - val comparable1 = parseNumericExpression("2 ^ 3").toComparableOperation() - val comparable2 = parseNumericExpression("2 ^ 4").toComparableOperation() + val comparable1 = parseNumericExpression("2 ^ 3").convertToComparableOperation() + val comparable2 = parseNumericExpression("2 ^ 4").convertToComparableOperation() assertThat(comparable1).isNotEqualTo(comparable2) } @@ -2647,9 +2647,9 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_exponentiationOps_sameOnlyByEvaluation_areNotEqual() { // Disable optional errors to allow nested exponentiation. - val comparable1 = parseNumericExpression("2 ^ 4").toComparableOperation() + val comparable1 = parseNumericExpression("2 ^ 4").convertToComparableOperation() val comparable2 = - parseNumericExpression("2 ^ 2 ^ 2", errorCheckingMode = REQUIRED_ONLY).toComparableOperation() + parseNumericExpression("2 ^ 2 ^ 2", errorCheckingMode = REQUIRED_ONLY).convertToComparableOperation() // While the two expressions are numerically equivalent, they aren't comparable since there are // extra terms in one (more than trivial rearranging is required to determine that they're @@ -2659,24 +2659,24 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_squareRootOps_same_areEqual() { - val comparable1 = parseNumericExpression("sqrt(2)").toComparableOperation() - val comparable2 = parseNumericExpression("sqrt(2)").toComparableOperation() + val comparable1 = parseNumericExpression("sqrt(2)").convertToComparableOperation() + val comparable2 = parseNumericExpression("sqrt(2)").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_squareRootOps_differentByValue_areNotEqual() { - val comparable1 = parseNumericExpression("sqrt(2)").toComparableOperation() - val comparable2 = parseNumericExpression("sqrt(3)").toComparableOperation() + val comparable1 = parseNumericExpression("sqrt(2)").convertToComparableOperation() + val comparable2 = parseNumericExpression("sqrt(3)").convertToComparableOperation() assertThat(comparable1).isNotEqualTo(comparable2) } @Test fun testEquals_squareRootOps_sameOnlyByEvaluation_areNotEqual() { - val comparable1 = parseNumericExpression("sqrt(2)").toComparableOperation() - val comparable2 = parseNumericExpression("sqrt(1 + 1)").toComparableOperation() + val comparable1 = parseNumericExpression("sqrt(2)").convertToComparableOperation() + val comparable2 = parseNumericExpression("sqrt(1 + 1)").convertToComparableOperation() // While the two expressions are numerically equivalent, they aren't comparable since there are // extra terms in one (more than trivial rearranging is required to determine that they're @@ -2686,40 +2686,40 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_additionsAndSubtractions_differentByOrder_areEqual() { - val comparable1 = parseNumericExpression("1+2-3").toComparableOperation() - val comparable2 = parseNumericExpression("-3+2+1").toComparableOperation() + val comparable1 = parseNumericExpression("1+2-3").convertToComparableOperation() + val comparable2 = parseNumericExpression("-3+2+1").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_multiplicationsAndDivisions_differentByOrder_areEqual() { - val comparable1 = parseNumericExpression("2*3/4*7").toComparableOperation() - val comparable2 = parseNumericExpression("7*2*3/4").toComparableOperation() + val comparable1 = parseNumericExpression("2*3/4*7").convertToComparableOperation() + val comparable2 = parseNumericExpression("7*2*3/4").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_allAccumulationOperations_differentByOrder_areEqual() { - val comparable1 = parseNumericExpression("1+2*3/4*7-8+3").toComparableOperation() - val comparable2 = parseNumericExpression("-8+3+7*3/4*2+1").toComparableOperation() + val comparable1 = parseNumericExpression("1+2*3/4*7-8+3").convertToComparableOperation() + val comparable2 = parseNumericExpression("-8+3+7*3/4*2+1").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_allOperations_differentByOrder_areEqual() { - val comparable1 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2*3)").toComparableOperation() - val comparable2 = parseNumericExpression("2^3*sqrt(3*2+1)/7-(-3*2+2)").toComparableOperation() + val comparable1 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2*3)").convertToComparableOperation() + val comparable2 = parseNumericExpression("2^3*sqrt(3*2+1)/7-(-3*2+2)").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_allOperations_oneNestedDifferentByValue_areNotEqual() { - val comparable1 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2*3)").toComparableOperation() - val comparable2 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-4*3)").toComparableOperation() + val comparable1 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2*3)").convertToComparableOperation() + val comparable2 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-4*3)").convertToComparableOperation() // Just one different term leads to the entire comparison failing. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2727,8 +2727,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_twoOps_allOperations_oneMissingTerm_areNotEqual() { - val comparable1 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2*3)").toComparableOperation() - val comparable2 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2)").toComparableOperation() + val comparable1 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2*3)").convertToComparableOperation() + val comparable2 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2)").convertToComparableOperation() // Just one missing term leads to the entire comparison failing. assertThat(comparable1).isNotEqualTo(comparable2) diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt index 586a7253c9d..85a734a7ac4 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt @@ -78,8 +78,8 @@ class MathExpressionExtensionsTest { val expression1 = parseAlgebraicExpression("x+2/x-(-7*8-9)+sqrt((x+2)+1)+3xy^2") val expression2 = parseAlgebraicExpression("sqrt(x+(1+2))+2/x+x-(-9+8*-7-3y^2x)") - val operation1 = expression1.toComparable() - val operation2 = expression2.toComparable() + val operation1 = expression1.toComparableOperation() + val operation2 = expression2.toComparableOperation() assertThat(operation1).isEqualTo(operation2) } @@ -89,8 +89,8 @@ class MathExpressionExtensionsTest { val expression1 = parseAlgebraicExpression("x+2/x-(-7*8-9)+sqrt((x+2)+1)+3xy^2") val expression2 = parseAlgebraicExpression("sqrt(x+(1+3))+2/x+x-(-9+8*-7-3y^2x)") - val operation1 = expression1.toComparable() - val operation2 = expression2.toComparable() + val operation1 = expression1.toComparableOperation() + val operation2 = expression2.toComparableOperation() assertThat(operation1).isNotEqualTo(operation2) } From 1970f347efe4ed65ff5f7ef820a5585d2d28f91d Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 28 Jan 2022 20:51:52 -0800 Subject: [PATCH 086/134] Renames & lint fixes. --- .../android/util/math/ComparatorExtensions.kt | 2 +- ...xpressionToComparableOperationConverter.kt | 11 +- ...ssionToComparableOperationConverterTest.kt | 149 +++++++++--------- 3 files changed, 84 insertions(+), 78 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt index 4e4355d0ec0..e853a03bfb9 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt @@ -39,7 +39,7 @@ fun Comparator.compareIterables(first: Iterable, second: Iterable): * [Comparator] isn't used). This short-circuiting behavior can be useful when comparing recursively * infinite proto structures to avoid stack overflows.. */ -fun Comparator.compareProtos(left: T, right: T): Int { +fun Comparator.compareProtos(left: T, right: T): Int { val defaultValue = left.defaultInstanceForType val leftIsDefault = left == defaultValue val rightIsDefault = right == defaultValue diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt index 5983f09e27d..429ffe85828 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt @@ -6,7 +6,6 @@ import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.A import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.SUMMATION import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation.BinaryOperation -import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE @@ -22,15 +21,16 @@ import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERA import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE import org.oppia.android.app.model.MathFunctionCall.FunctionType import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT -import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator /** * Converter from [MathExpression] to [ComparableOperation]. * - * See the separate proto details for context, and [convertToComparableOperation] for the actual conversion - * function. + * See the separate proto details for context, and [convertToComparableOperation] for the actual + * conversion function. */ class ExpressionToComparableOperationConverter private constructor() { companion object { @@ -139,7 +139,8 @@ class ExpressionToComparableOperationConverter private constructor() { addOperationToSum(expression.unaryOperation.operand, forceNegative) // Skip groups so that nested operations can be properly combined. expression.expressionTypeCase == GROUP -> addOperationToSum(expression.group, forceNegative) - forceNegative -> addCombinedOperations(expression.convertToComparableOperation().invertNegation()) + forceNegative -> + addCombinedOperations(expression.convertToComparableOperation().invertNegation()) else -> addCombinedOperations(expression.convertToComparableOperation()) } } diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt index 1452a9c70bd..4af4b5afaa9 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt @@ -2,8 +2,8 @@ package org.oppia.android.util.math import com.google.common.truth.Truth.assertThat import org.junit.Test -import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.convertToComparableOperation import org.junit.runner.RunWith +import org.oppia.android.app.model.ComparableOperation import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.PRODUCT import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.SUMMATION import org.oppia.android.app.model.MathExpression @@ -12,6 +12,7 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized import org.oppia.android.testing.math.ComparableOperationSubject.Companion.assertThat +import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.convertToComparableOperation import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.REQUIRED_ONLY @@ -1665,7 +1666,7 @@ class ExpressionToComparableOperationConverterTest { } } } - + /* Non-commutative sorting */ @Test @@ -2439,46 +2440,46 @@ class ExpressionToComparableOperationConverterTest { * reliable equivalence checking (and may instead require threshold checking for approximated * equivalence). * - * Further, these checks are using vanilla equivalence checking since they rely on the opreations + * Further, these checks are using vanilla equivalence checking since they rely on the operations * being properly sorted. */ @Test fun testEquals_additionOps_differentByCommutativity_areEqual() { - val comparable1 = parseNumericExpression("1 + 2").convertToComparableOperation() - val comparable2 = parseNumericExpression("2 + 1").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("1 + 2") + val comparable2 = parseNumericExpressionAsComparableOperation("2 + 1") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_additionOps_differentByAssociativity_areEqual() { - val comparable1 = parseNumericExpression("1 + (2 + 3)").convertToComparableOperation() - val comparable2 = parseNumericExpression("(1 + 2) + 3").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("1 + (2 + 3)") + val comparable2 = parseNumericExpressionAsComparableOperation("(1 + 2) + 3") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_additionOps_differentByAssociativityAndCommutativity_areEqual() { - val comparable1 = parseNumericExpression("1 + (2 + 3)").convertToComparableOperation() - val comparable2 = parseNumericExpression("(2 + 1) + 3").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("1 + (2 + 3)") + val comparable2 = parseNumericExpressionAsComparableOperation("(2 + 1) + 3") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_additionOps_differentByValue_areNotEqual() { - val comparable1 = parseNumericExpression("1 + 2").convertToComparableOperation() - val comparable2 = parseNumericExpression("1 + 3").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("1 + 2") + val comparable2 = parseNumericExpressionAsComparableOperation("1 + 3") assertThat(comparable1).isNotEqualTo(comparable2) } @Test fun testEquals_additionOps_sameOnlyByEvaluation_areNotEqual() { - val comparable1 = parseNumericExpression("1 + 2 + 2 + 1").convertToComparableOperation() - val comparable2 = parseNumericExpression("1 + 2 + 3").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("1 + 2 + 2 + 1") + val comparable2 = parseNumericExpressionAsComparableOperation("1 + 2 + 3") // While the two expressions are numerically equivalent, they aren't comparable since there are // extra terms in one (more than trivial rearranging is required to determine that they're @@ -2488,40 +2489,40 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_multiplicationOps_differentByCommutativity_areEqual() { - val comparable1 = parseNumericExpression("2 * 3").convertToComparableOperation() - val comparable2 = parseNumericExpression("3 * 2").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 * 3") + val comparable2 = parseNumericExpressionAsComparableOperation("3 * 2") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_multiplicationOps_differentByAssociativity_areEqual() { - val comparable1 = parseNumericExpression("2 * (3 * 4)").convertToComparableOperation() - val comparable2 = parseNumericExpression("(2 * 3) * 4").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 * (3 * 4)") + val comparable2 = parseNumericExpressionAsComparableOperation("(2 * 3) * 4") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_multiplicationOps_differentByAssociativityAndCommutativity_areEqual() { - val comparable1 = parseNumericExpression("2 * (3 * 4)").convertToComparableOperation() - val comparable2 = parseNumericExpression("(3 * 2) * 4").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 * (3 * 4)") + val comparable2 = parseNumericExpressionAsComparableOperation("(3 * 2) * 4") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_multiplicationOps_differentByValue_areNotEqual() { - val comparable1 = parseNumericExpression("2 * 3").convertToComparableOperation() - val comparable2 = parseNumericExpression("2 * 4").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 * 3") + val comparable2 = parseNumericExpressionAsComparableOperation("2 * 4") assertThat(comparable1).isNotEqualTo(comparable2) } @Test fun testEquals_multiplicationOps_sameOnlyByEvaluation_areNotEqual() { - val comparable1 = parseNumericExpression("2 * 3 * 4").convertToComparableOperation() - val comparable2 = parseNumericExpression("2 * 2 * 2 * 3").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 * 3 * 4") + val comparable2 = parseNumericExpressionAsComparableOperation("2 * 2 * 2 * 3") // While the two expressions are numerically equivalent, they aren't comparable since there are // extra terms in one (more than trivial rearranging is required to determine that they're @@ -2531,16 +2532,16 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_subtractionOps_same_areEqual() { - val comparable1 = parseNumericExpression("1 - 2").convertToComparableOperation() - val comparable2 = parseNumericExpression("1 - 2").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("1 - 2") + val comparable2 = parseNumericExpressionAsComparableOperation("1 - 2") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_subtractionOps_differentByOrder_areNotEqual() { - val comparable1 = parseNumericExpression("1 - 2").convertToComparableOperation() - val comparable2 = parseNumericExpression("2 - 1").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("1 - 2") + val comparable2 = parseNumericExpressionAsComparableOperation("2 - 1") // Subtraction is not commutative. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2548,8 +2549,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_subtractionOps_differentByAssociativity_areNotEqual() { - val comparable1 = parseNumericExpression("1 - (2 - 3)").convertToComparableOperation() - val comparable2 = parseNumericExpression("(1 - 2) - 3").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("1 - (2 - 3)") + val comparable2 = parseNumericExpressionAsComparableOperation("(1 - 2) - 3") // Subtraction is not associative. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2557,8 +2558,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_subtractionOps_sameOnlyByEvaluation_areNotEqual() { - val comparable1 = parseNumericExpression("1 - 2 - 3").convertToComparableOperation() - val comparable2 = parseNumericExpression("1 - 2 - 2 - 1").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("1 - 2 - 3") + val comparable2 = parseNumericExpressionAsComparableOperation("1 - 2 - 2 - 1") // While the two expressions are numerically equivalent, they aren't comparable since there are // extra terms in one (more than trivial rearranging is required to determine that they're @@ -2568,16 +2569,16 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_divisionOps_same_areEqual() { - val comparable1 = parseNumericExpression("2 / 3").convertToComparableOperation() - val comparable2 = parseNumericExpression("2 / 3").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 / 3") + val comparable2 = parseNumericExpressionAsComparableOperation("2 / 3") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_divisionOps_differentByOrder_areNotEqual() { - val comparable1 = parseNumericExpression("2 / 3").convertToComparableOperation() - val comparable2 = parseNumericExpression("3 / 2").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 / 3") + val comparable2 = parseNumericExpressionAsComparableOperation("3 / 2") // Division is not commutative. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2585,8 +2586,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_divisionOps_differentByAssociativity_areNotEqual() { - val comparable1 = parseNumericExpression("2 / (3 / 4)").convertToComparableOperation() - val comparable2 = parseNumericExpression("(2 / 3) / 4").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 / (3 / 4)") + val comparable2 = parseNumericExpressionAsComparableOperation("(2 / 3) / 4") // Division is not associative. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2594,8 +2595,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_divisionOps_sameOnlyByEvaluation_areNotEqual() { - val comparable1 = parseNumericExpression("2 / 3 / 4").convertToComparableOperation() - val comparable2 = parseNumericExpression("2 / 3 / 2 / 2").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 / 3 / 4") + val comparable2 = parseNumericExpressionAsComparableOperation("2 / 3 / 2 / 2") // While the two expressions are numerically equivalent, they aren't comparable since there are // extra terms in one (more than trivial rearranging is required to determine that they're @@ -2605,16 +2606,16 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_exponentiationOps_same_areEqual() { - val comparable1 = parseNumericExpression("2 ^ 3").convertToComparableOperation() - val comparable2 = parseNumericExpression("2 ^ 3").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 ^ 3") + val comparable2 = parseNumericExpressionAsComparableOperation("2 ^ 3") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_exponentiationOps_differentByOrder_areNotEqual() { - val comparable1 = parseNumericExpression("2 ^ 3").convertToComparableOperation() - val comparable2 = parseNumericExpression("3 ^ 2").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 ^ 3") + val comparable2 = parseNumericExpressionAsComparableOperation("3 ^ 2") // Exponentiation is not commutative. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2624,13 +2625,9 @@ class ExpressionToComparableOperationConverterTest { fun testEquals_exponentiationOps_differentByAssociativity_areNotEqual() { // Disable optional errors to allow nested exponentiation. val comparable1 = - parseNumericExpression( - "2 ^ (3 ^ 4)", errorCheckingMode = REQUIRED_ONLY - ).convertToComparableOperation() + parseNumericExpressionAsComparableOperation("2 ^ (3 ^ 4)", errorCheckingMode = REQUIRED_ONLY) val comparable2 = - parseNumericExpression( - "(2 ^ 3) ^ 4", errorCheckingMode = REQUIRED_ONLY - ).convertToComparableOperation() + parseNumericExpressionAsComparableOperation("(2 ^ 3) ^ 4", errorCheckingMode = REQUIRED_ONLY) // Exponentiation is not associative. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2638,8 +2635,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_exponentiationOps_differentByValue_areNotEqual() { - val comparable1 = parseNumericExpression("2 ^ 3").convertToComparableOperation() - val comparable2 = parseNumericExpression("2 ^ 4").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 ^ 3") + val comparable2 = parseNumericExpressionAsComparableOperation("2 ^ 4") assertThat(comparable1).isNotEqualTo(comparable2) } @@ -2647,9 +2644,9 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_exponentiationOps_sameOnlyByEvaluation_areNotEqual() { // Disable optional errors to allow nested exponentiation. - val comparable1 = parseNumericExpression("2 ^ 4").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 ^ 4") val comparable2 = - parseNumericExpression("2 ^ 2 ^ 2", errorCheckingMode = REQUIRED_ONLY).convertToComparableOperation() + parseNumericExpressionAsComparableOperation("2 ^ 2 ^ 2", errorCheckingMode = REQUIRED_ONLY) // While the two expressions are numerically equivalent, they aren't comparable since there are // extra terms in one (more than trivial rearranging is required to determine that they're @@ -2659,24 +2656,24 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_squareRootOps_same_areEqual() { - val comparable1 = parseNumericExpression("sqrt(2)").convertToComparableOperation() - val comparable2 = parseNumericExpression("sqrt(2)").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("sqrt(2)") + val comparable2 = parseNumericExpressionAsComparableOperation("sqrt(2)") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_squareRootOps_differentByValue_areNotEqual() { - val comparable1 = parseNumericExpression("sqrt(2)").convertToComparableOperation() - val comparable2 = parseNumericExpression("sqrt(3)").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("sqrt(2)") + val comparable2 = parseNumericExpressionAsComparableOperation("sqrt(3)") assertThat(comparable1).isNotEqualTo(comparable2) } @Test fun testEquals_squareRootOps_sameOnlyByEvaluation_areNotEqual() { - val comparable1 = parseNumericExpression("sqrt(2)").convertToComparableOperation() - val comparable2 = parseNumericExpression("sqrt(1 + 1)").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("sqrt(2)") + val comparable2 = parseNumericExpressionAsComparableOperation("sqrt(1 + 1)") // While the two expressions are numerically equivalent, they aren't comparable since there are // extra terms in one (more than trivial rearranging is required to determine that they're @@ -2686,40 +2683,40 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_additionsAndSubtractions_differentByOrder_areEqual() { - val comparable1 = parseNumericExpression("1+2-3").convertToComparableOperation() - val comparable2 = parseNumericExpression("-3+2+1").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("1+2-3") + val comparable2 = parseNumericExpressionAsComparableOperation("-3+2+1") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_multiplicationsAndDivisions_differentByOrder_areEqual() { - val comparable1 = parseNumericExpression("2*3/4*7").convertToComparableOperation() - val comparable2 = parseNumericExpression("7*2*3/4").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2*3/4*7") + val comparable2 = parseNumericExpressionAsComparableOperation("7*2*3/4") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_allAccumulationOperations_differentByOrder_areEqual() { - val comparable1 = parseNumericExpression("1+2*3/4*7-8+3").convertToComparableOperation() - val comparable2 = parseNumericExpression("-8+3+7*3/4*2+1").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("1+2*3/4*7-8+3") + val comparable2 = parseNumericExpressionAsComparableOperation("-8+3+7*3/4*2+1") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_allOperations_differentByOrder_areEqual() { - val comparable1 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2*3)").convertToComparableOperation() - val comparable2 = parseNumericExpression("2^3*sqrt(3*2+1)/7-(-3*2+2)").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("sqrt(1+2*3)*2^3/7-(2-2*3)") + val comparable2 = parseNumericExpressionAsComparableOperation("2^3*sqrt(3*2+1)/7-(-3*2+2)") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_allOperations_oneNestedDifferentByValue_areNotEqual() { - val comparable1 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2*3)").convertToComparableOperation() - val comparable2 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-4*3)").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("sqrt(1+2*3)*2^3/7-(2-2*3)") + val comparable2 = parseNumericExpressionAsComparableOperation("sqrt(1+2*3)*2^3/7-(2-4*3)") // Just one different term leads to the entire comparison failing. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2727,16 +2724,24 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_twoOps_allOperations_oneMissingTerm_areNotEqual() { - val comparable1 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2*3)").convertToComparableOperation() - val comparable2 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2)").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("sqrt(1+2*3)*2^3/7-(2-2*3)") + val comparable2 = parseNumericExpressionAsComparableOperation("sqrt(1+2*3)*2^3/7-(2-2)") // Just one missing term leads to the entire comparison failing. assertThat(comparable1).isNotEqualTo(comparable2) } + private fun parseNumericExpressionAsComparableOperation( + expression: String, + errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + ): ComparableOperation { + return parseNumericExpression(expression, errorCheckingMode).convertToComparableOperation() + } + private companion object { private fun parseNumericExpression( - expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + expression: String, + errorCheckingMode: ErrorCheckingMode = ALL_ERRORS ): MathExpression { return MathExpressionParser.parseNumericExpression( expression, errorCheckingMode From 45b6099465f12eb09a644c43a8d8887d1e6bb68f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 28 Jan 2022 21:25:14 -0800 Subject: [PATCH 087/134] Post-merge fixes. --- .../android/util/math/PolynomialExtensions.kt | 26 +++++++++++++++++++ .../org/oppia/android/util/math/BUILD.bazel | 2 +- .../util/math/ExpressionToPolynomialTest.kt | 4 +-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index 39ad485955d..534253c02c4 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -393,3 +393,29 @@ private fun Real.maybeSimplifyRationalToInteger(): Real = when (realTypeCase) { Real.RealTypeCase.IRRATIONAL, Real.RealTypeCase.INTEGER, Real.RealTypeCase.REALTYPE_NOT_SET, null -> this } + +// TODO: figure out of this can be removed. +private fun > Comparator.thenComparingReversed( + keySelector: (T) -> U +): Comparator = thenComparing(Comparator.comparing(keySelector).reversed()) + +// TODO: figure out of this can be removed. +private fun Comparator.toSetComparator(): Comparator> { + val itemComparator = this + return Comparator { first, second -> + // Reference: https://stackoverflow.com/a/30107086. + val firstIter = first.iterator() + val secondIter = second.iterator() + while (firstIter.hasNext() && secondIter.hasNext()) { + val comparison = itemComparator.compare(firstIter.next(), secondIter.next()) + if (comparison != 0) return@Comparator comparison // Found a different item. + } + + // Everything is equal up to here, see if the lists are different length. + return@Comparator when { + firstIter.hasNext() -> 1 // The first list is longer, therefore "greater." + secondIter.hasNext() -> -1 // Ditto, but for the second list. + else -> 0 // Otherwise, they're the same length with all equal items (and are thus equal). + } + } +} 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 index 98cc029ec01..519cff70cf8 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -185,7 +185,7 @@ oppia_android_test( "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:parser", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt index f8163f88c3c..b66c801e1ba 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt @@ -779,8 +779,8 @@ class ExpressionToPolynomialTest { assertThat(poly44).term(1).apply { hasCoefficientThat().isRationalThat().apply { hasNegativePropertyThat().isTrue() - hasWholeNumberThat().isEqualTo(0) - hasNumeratorThat().isEqualTo(3) + hasWholeNumberThat().isEqualTo(1) + hasNumeratorThat().isEqualTo(1) hasDenominatorThat().isEqualTo(2) } hasVariableCountThat().isEqualTo(0) From 76a788774f6eeb441933b3f5858a9e58d2c97545 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 1 Feb 2022 18:57:46 -0800 Subject: [PATCH 088/134] Add tests. --- .../math/ExpressionToPolynomialConverter.kt | 14 +- .../util/math/MathExpressionExtensions.kt | 26 +- .../android/util/math/PolynomialExtensions.kt | 263 +- .../oppia/android/util/math/RealExtensions.kt | 52 +- .../org/oppia/android/util/math/BUILD.bazel | 40 +- .../ExpressionToPolynomialConverterTest.kt | 2325 +++++++++++++++ .../util/math/ExpressionToPolynomialTest.kt | 945 ------ .../util/math/MathExpressionExtensionsTest.kt | 42 +- .../util/math/PolynomialExtensionsTest.kt | 2653 ++++++++++++++++- .../android/util/math/RealExtensionsTest.kt | 402 ++- 10 files changed, 5514 insertions(+), 1248 deletions(-) create mode 100644 utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialConverterTest.kt delete mode 100644 utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt index 15b9678626b..e64aa1baa63 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt @@ -23,11 +23,14 @@ import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE import org.oppia.android.app.model.Polynomial import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator +import org.oppia.android.app.model.Real class ExpressionToPolynomialConverter private constructor() { companion object { + // TODO: document that this generally only relate to algebraic expressions. fun MathExpression.reduceToPolynomial(): Polynomial? = - replaceSquareRoots().reduceToPolynomialAux() + replaceSquareRoots() + .reduceToPolynomialAux() ?.removeUnnecessaryVariables() ?.simplifyRationals() ?.sort() @@ -59,6 +62,7 @@ class ExpressionToPolynomialConverter private constructor() { }.build() FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> this } + // This also eliminates groups from the expression. GROUP -> group.replaceSquareRoots() CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> this } @@ -83,7 +87,7 @@ class ExpressionToPolynomialConverter private constructor() { SUBTRACT -> leftPolynomial - rightPolynomial MULTIPLY -> leftPolynomial * rightPolynomial DIVIDE -> leftPolynomial / rightPolynomial - EXPONENTIATE -> leftPolynomial.pow(rightPolynomial) + EXPONENTIATE -> leftPolynomial pow rightPolynomial BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> null } } @@ -109,5 +113,11 @@ class ExpressionToPolynomialConverter private constructor() { }.build() ) } + + private fun createConstantPolynomial(constant: Real): Polynomial = + createSingleTermPolynomial(Polynomial.Term.newBuilder().setCoefficient(constant).build()) + + private fun createSingleTermPolynomial(term: Polynomial.Term): Polynomial = + Polynomial.newBuilder().apply { addTerm(term) }.build() } } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 1c45ad9009c..65d40515f5f 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -47,28 +47,4 @@ fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() */ fun MathExpression.toComparableOperation(): ComparableOperation = convertToComparableOperation() -fun MathExpression.toPolynomial(): Polynomial? = stripGroups().reduceToPolynomial() - -// TODO: remove similar to comparable operations. -private fun MathExpression.stripGroups(): MathExpression { - return when (expressionTypeCase) { - BINARY_OPERATION -> toBuilder().apply { - binaryOperation = binaryOperation.toBuilder().apply { - leftOperand = binaryOperation.leftOperand.stripGroups() - rightOperand = binaryOperation.rightOperand.stripGroups() - }.build() - }.build() - UNARY_OPERATION -> toBuilder().apply { - unaryOperation = unaryOperation.toBuilder().apply { - operand = unaryOperation.operand.stripGroups() - }.build() - }.build() - FUNCTION_CALL -> toBuilder().apply { - functionCall = functionCall.toBuilder().apply { - argument = functionCall.argument.stripGroups() - }.build() - }.build() - GROUP -> group.stripGroups() - CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> this - } -} +fun MathExpression.toPolynomial(): Polynomial? = reduceToPolynomial() diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index 534253c02c4..e64f0aaabab 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -7,6 +7,11 @@ import org.oppia.android.app.model.Polynomial.Term.Variable import org.oppia.android.app.model.Real import java.util.SortedSet +val ZERO_POLYNOMIAL: Polynomial = createConstantPolynomial(ZERO) + +val ONE_POLYNOMIAL: Polynomial = createConstantPolynomial(ONE) + +// TODO: Kotlin-ify. private val POLYNOMIAL_VARIABLE_COMPARATOR: Comparator by lazy { // Note that power is reversed because larger powers should actually be sorted ahead of smaller // powers for the same variable name (but variable name still takes precedence). This ensures @@ -27,17 +32,12 @@ private val POLYNOMIAL_TERM_COMPARATOR: Comparator by lazy { Comparator.comparing>( { term -> term.variableList.toSortedSet(POLYNOMIAL_VARIABLE_COMPARATOR) }, POLYNOMIAL_VARIABLE_COMPARATOR.reversed().toSetComparator() - ).reversed().thenComparing(Term::getCoefficient, REAL_COMPARATOR) + ).reversed().thenComparing(Term::getCoefficient, REAL_COMPARATOR.reversed()) } /** Returns whether this polynomial is a constant-only polynomial (contains no variables). */ fun Polynomial.isConstant(): Boolean = termCount == 1 && getTerm(0).variableCount == 0 -fun Polynomial.isSingleTerm(): Boolean = termList.size == 1 - -fun Polynomial.isApproximatelyZero(): Boolean = - termList.all { it.coefficient.isApproximatelyZero() } // Zero polynomials only have 0 coefs. - /** * Returns the first term coefficient from this polynomial. This corresponds to the whole value of * the polynomial iff isConstant() returns true, otherwise this value isn't useful. @@ -47,10 +47,6 @@ fun Polynomial.isApproximatelyZero(): Boolean = */ fun Polynomial.getConstant(): Real = getTerm(0).coefficient -// Return the highest power to represent the degree of the polynomial. Reference: -// https://www.varsitytutors.com/algebra_1-help/how-to-find-the-degree-of-a-polynomial. -fun Polynomial.getDegree(): Int = getLeadingTerm().highestDegree() - /** * Returns a human-readable, plaintext representation of this [Polynomial]. * @@ -67,57 +63,6 @@ fun Polynomial.toPlainText(): String { } } -private fun Term.toPlainText(): String { - val productValues = mutableListOf() - - // Include the coefficient if there is one (coefficients of 1 are ignored only if there are - // variables present). - productValues += when { - variableList.isEmpty() || !abs(coefficient).isApproximatelyEqualTo(1.0) -> when { - coefficient.isRational() && variableList.isNotEmpty() -> "(${coefficient.toPlainText()})" - else -> coefficient.toPlainText() - } - coefficient.isNegative() -> "-" - else -> "" - } - - // Include any present variables. - productValues += variableList.map(Variable::toPlainText) - - // Take the product of all relevant values of the term. - return productValues.joinToString(separator = "") -} - -private fun Variable.toPlainText(): String { - return if (power > 1) "$name^$power" else name -} - -fun Polynomial.combineLikeTerms(): Polynomial { - // The following algorithm is expected to grow in space by O(N*M) and in time by O(N*m*log(m)) - // where N is the total number of terms, M is the total number of variables, and m is the largest - // single count of variables among all terms (this is assuming constant-time insertion for the - // underlying hashtable). - val newTerms = termList.groupBy { - it.variableList.sortedWith(POLYNOMIAL_VARIABLE_COMPARATOR) - }.mapValues { (_, coefficientTerms) -> - coefficientTerms.map { it.coefficient } - }.mapNotNull { (variables, coefficients) -> - // Combine like terms by summing their coefficients. - val newCoefficient = coefficients.reduce(Real::plus) - return@mapNotNull if (!newCoefficient.isApproximatelyZero()) { - Term.newBuilder().apply { - coefficient = newCoefficient - - // Remove variables with zero powers (since they evaluate to '1'). - addAllVariable(variables.filter { variable -> variable.power != 0 }) - }.build() - } else null // Zero terms should be removed. - } - return Polynomial.newBuilder().apply { - addAllTerm(newTerms) - }.build().ensureAtLeastConstant() -} - fun Polynomial.removeUnnecessaryVariables(): Polynomial { return Polynomial.newBuilder().apply { addAllTerm( @@ -141,7 +86,17 @@ fun Polynomial.simplifyRationals(): Polynomial { } fun Polynomial.sort(): Polynomial = Polynomial.newBuilder().apply { - addAllTerm(this@sort.termList.sortedWith(POLYNOMIAL_TERM_COMPARATOR)) + // The double sorting here is less efficient, but it ensures both terms and variables are + // correctly kept sorted. Fortunately, most internal operations will keep variables sorted by + // default. + addAllTerm( + this@sort.termList.map { term -> + Term.newBuilder().apply { + coefficient = term.coefficient + addAllVariable(term.variableList.sortedWith(POLYNOMIAL_VARIABLE_COMPARATOR)) + }.build() + }.sortedWith(POLYNOMIAL_TERM_COMPARATOR) + ) }.build() operator fun Polynomial.unaryMinus(): Polynomial { @@ -157,7 +112,7 @@ operator fun Polynomial.plus(rhs: Polynomial): Polynomial { // common terms). return Polynomial.newBuilder().apply { addAllTerm(this@plus.termList + rhs.termList) - }.build().combineLikeTerms().removeUnnecessaryVariables() + }.build().combineLikeTerms().simplifyRationals().removeUnnecessaryVariables() } operator fun Polynomial.minus(rhs: Polynomial): Polynomial { @@ -172,8 +127,11 @@ operator fun Polynomial.times(rhs: Polynomial): Polynomial { } // Treat each multiplied term as a unique polynomial, then add them together (so that like terms - // can be properly combined). - return crossMultipliedTerms.map { createSingleTermPolynomial(it) }.reduce(Polynomial::plus) + // can be properly combined). Finally, ensure unnecessary variables are eliminated (especially for + // cases where no addition takes place, such as 0*x). + return crossMultipliedTerms.map { + createSingleTermPolynomial(it) + }.reduce(Polynomial::plus).simplifyRationals().removeUnnecessaryVariables() } operator fun Polynomial.div(rhs: Polynomial): Polynomial? { @@ -182,60 +140,90 @@ operator fun Polynomial.div(rhs: Polynomial): Polynomial? { return null // Dividing by zero is invalid and thus cannot yield a polynomial. } - var quotient = createConstantPolynomial(ZERO) + var quotient = ZERO_POLYNOMIAL var remainder = this - val leadingDivisorTerm = rhs.getLeadingTerm() + val leadingDivisorTerm = rhs.getLeadingTerm() ?: return null val divisorVariable = leadingDivisorTerm.highestDegreeVariable() val divisorVariableName = divisorVariable?.name val divisorDegree = leadingDivisorTerm.highestDegree() - while (!remainder.isApproximatelyZero() && remainder.getDegree() >= divisorDegree) { + while (!remainder.isApproximatelyZero() + && (remainder.getDegree() ?: return null) >= divisorDegree) { // Attempt to divide the leading terms (this may fail). Note that the leading term should always // be based on the divisor variable being used (otherwise subsequent division steps will be // inconsistent and potentially fail to resolve). - val newTerm = - remainder.getLeadingTerm(matchedVariable = divisorVariableName) / leadingDivisorTerm - ?: return null + val remainingLeadingTerm = remainder.getLeadingTerm(matchedVariable = divisorVariableName) + val newTerm = remainingLeadingTerm?.div(leadingDivisorTerm) ?: return null quotient += newTerm.toPolynomial() remainder -= newTerm.toPolynomial() * rhs } - return when { - remainder.isApproximatelyZero() -> quotient // Exact division (i.e. with no remainder). - remainder.isConstant() && rhs.isConstant() -> { - // Remainder is a constant term. - val remainingTerm = remainder.getConstant() / rhs.getConstant() - quotient + createConstantPolynomial(remainingTerm) - } - else -> null // Remainder is a polynomial, so the division failed. - } + // Either the division was exact, or the remainder is a polynomial (i.e. a failed division). + return quotient.takeIf { remainder.isApproximatelyZero() } } -fun Polynomial.pow(exp: Polynomial): Polynomial? { +infix fun Polynomial.pow(exp: Polynomial): Polynomial? { // Polynomial exponentiation is only supported if the right side is a constant polynomial, // otherwise the result cannot be a polynomial (though could still be compared to another // expression by utilizing sampling techniques). - return if (exp.isConstant()) pow(exp.getConstant()) else null + return if (exp.isConstant()) { + pow(exp.getConstant())?.simplifyRationals()?.removeUnnecessaryVariables() + } else null } -fun createConstantPolynomial(constant: Real): Polynomial = +private fun createConstantPolynomial(constant: Real): Polynomial = createSingleTermPolynomial(Term.newBuilder().setCoefficient(constant).build()) -fun createSingleTermPolynomial(term: Term): Polynomial = +private fun createSingleTermPolynomial(term: Term): Polynomial = Polynomial.newBuilder().apply { addTerm(term) }.build() -private fun Polynomial.pow(exp: Int): Polynomial { - // Anything raised to the power of 0 is 1. - if (exp == 0) return createConstantPolynomial(ONE) - if (exp == 1) return this - var newValue = this - for (i in 1 until exp) newValue *= this - return newValue +private fun Term.toPlainText(): String { + val productValues = mutableListOf() + + // Include the coefficient if there is one (coefficients of 1 are ignored only if there are + // variables present). + productValues += when { + variableList.isEmpty() || !abs(coefficient).isApproximatelyEqualTo(1.0) -> when { + coefficient.isRational() && variableList.isNotEmpty() -> "(${coefficient.toPlainText()})" + else -> coefficient.toPlainText() + } + coefficient.isNegative() -> "-" + else -> "" + } + + // Include any present variables. + productValues += variableList.map(Variable::toPlainText) + + // Take the product of all relevant values of the term. + return productValues.joinToString(separator = "") } -private fun Polynomial.pow(rational: Fraction): Polynomial? { - // Polynomials with addition require factoring. - return if (isSingleTerm()) { - termList.first().pow(rational)?.toPolynomial() - } else null +private fun Variable.toPlainText(): String { + return if (power > 1) "$name^$power" else name +} + +private fun Polynomial.combineLikeTerms(): Polynomial { + // The following algorithm is expected to grow in space by O(N*M) and in time by O(N*m*log(m)) + // where N is the total number of terms, M is the total number of variables, and m is the largest + // single count of variables among all terms (this is assuming constant-time insertion for the + // underlying hashtable). + val newTerms = termList.groupBy { + it.variableList.sortedWith(POLYNOMIAL_VARIABLE_COMPARATOR) + }.mapValues { (_, coefficientTerms) -> + coefficientTerms.map { it.coefficient } + }.mapNotNull { (variables, coefficients) -> + // Combine like terms by summing their coefficients. + val newCoefficient = coefficients.reduce(Real::plus) + return@mapNotNull if (!newCoefficient.isApproximatelyZero()) { + Term.newBuilder().apply { + coefficient = newCoefficient + + // Remove variables with zero powers (since they evaluate to '1'). + addAllVariable(variables.filter { variable -> variable.power != 0 }) + }.build() + } else null // Zero terms should be removed. + } + return Polynomial.newBuilder().apply { + addAllTerm(newTerms) + }.build().ensureAtLeastConstant() } private fun Polynomial.pow(exp: Real): Polynomial? { @@ -243,7 +231,7 @@ private fun Polynomial.pow(exp: Real): Polynomial? { val positivePower = if (shouldBeInverted) -exp else exp val exponentiation = when { // Constant polynomials can be raised by any constant. - isConstant() -> createConstantPolynomial(getConstant().pow(positivePower)) + isConstant() -> (getConstant() pow positivePower)?.let { createConstantPolynomial(it) } // Polynomials can only be raised to positive integers (or zero). exp.isWholeNumber() -> exp.asWholeNumber()?.let { pow(it) } @@ -251,12 +239,12 @@ private fun Polynomial.pow(exp: Real): Polynomial? { // Polynomials can potentially be raised by a fractional power. exp.isRational() -> pow(exp.rational) - // All other cases require factoring will definitely not compute to polynomials (such as + // All other cases require factoring most likely will not compute to polynomials (such as // irrational exponents). else -> null } return if (shouldBeInverted) { - val onePolynomial = createConstantPolynomial(ONE) + val onePolynomial = ONE_POLYNOMIAL // Note that this division is guaranteed to fail if the exponentiation result is a polynomial. // Future implementations may leverage root-finding algorithms to factor for integer inverse // powers (such as square root, cubic root, etc.). Non-integer inverse powers will require @@ -265,6 +253,22 @@ private fun Polynomial.pow(exp: Real): Polynomial? { } else exponentiation } +private fun Polynomial.pow(rational: Fraction): Polynomial? { + // Polynomials with addition require factoring. + return if (isSingleTerm()) { + termList.first().pow(rational)?.toPolynomial() + } else null +} + +private fun Polynomial.pow(exp: Int): Polynomial { + // Anything raised to the power of 0 is 1. + if (exp == 0) return ONE_POLYNOMIAL + if (exp == 1) return this + var newValue = this + for (i in 1 until exp) newValue *= this + return newValue +} + private operator fun Term.times(rhs: Term): Term { // The coefficients are always multiplied. val combinedCoefficient = coefficient * rhs.coefficient @@ -324,14 +328,14 @@ private fun Term.pow(rational: Fraction): Term? { // term in question is not rootable to that degree. if (newVariablePowers.any { !it.isOnlyWholeNumber() }) return null + val newCoefficient = coefficient pow Real.newBuilder().apply { + this.rational = rational + }.build() ?: return null + return Term.newBuilder().apply { - coefficient = this@pow.coefficient.pow( - Real.newBuilder().apply { - this.rational = rational - }.build() - ) + coefficient = newCoefficient addAllVariable( - this@pow.variableList.zip(newVariablePowers).map { (variable, newPower) -> + (this@pow.variableList zip newVariablePowers).map { (variable, newPower) -> variable.toBuilder().apply { power = newPower.toWholeNumber() }.build() @@ -340,25 +344,30 @@ private fun Term.pow(rational: Fraction): Term? { }.build() } +/** + * Returns either this [Polynomial] or [ZERO_POLYNOMIAL] if this polynomial has no terms (i.e. the + * returned polynomial is always guaranteed to have at least one term). + */ private fun Polynomial.ensureAtLeastConstant(): Polynomial { - return if (termCount == 0) { - Polynomial.newBuilder().apply { - addTerm( - Term.newBuilder().apply { - coefficient = ZERO - }.build() - ) - }.build() - } else this + return if (termCount != 0) this else ZERO_POLYNOMIAL } -private fun Polynomial.getLeadingTerm(matchedVariable: String? = null): Term { +private fun Polynomial.isSingleTerm(): Boolean = termList.size == 1 + +private fun Polynomial.isApproximatelyZero(): Boolean = + termList.all { it.coefficient.isApproximatelyZero() } // Zero polynomials only have 0 coefs. + +// Return the highest power to represent the degree of the polynomial. Reference: +// https://www.varsitytutors.com/algebra_1-help/how-to-find-the-degree-of-a-polynomial. +private fun Polynomial.getDegree(): Int? = getLeadingTerm()?.highestDegree() + +private fun Polynomial.getLeadingTerm(matchedVariable: String? = null): Term? { // Return the leading term. Reference: https://undergroundmathematics.org/glossary/leading-term. return termList.filter { term -> matchedVariable?.let { variableName -> term.variableList.any { it.name == variableName } } ?: true - }.reduce { maxTerm, term -> + }.takeIf { it.isNotEmpty() }?.reduce { maxTerm, term -> val maxTermDegree = maxTerm.highestDegree() val termDegree = term.highestDegree() return@reduce if (termDegree > maxTermDegree) term else maxTerm @@ -383,11 +392,23 @@ private fun Map.toVariableList(): List { private fun Real.maybeSimplifyRationalToInteger(): Real = when (realTypeCase) { Real.RealTypeCase.RATIONAL -> { - if (rational.isOnlyWholeNumber()) { - Real.newBuilder().apply { - integer = this@maybeSimplifyRationalToInteger.rational.toWholeNumber() - }.build() - } else this + val improperRational = rational.toImproperForm() + when { + rational.isOnlyWholeNumber() -> { + Real.newBuilder().apply { + integer = this@maybeSimplifyRationalToInteger.rational.toWholeNumber() + }.build() + } + // Some fractions are effectively whole numbers. + improperRational.denominator == 1 -> { + Real.newBuilder().apply { + integer = if (improperRational.isNegative) { + -improperRational.numerator + } else improperRational.numerator + }.build() + } + else -> this + } } // Nothing to do in these cases. Real.RealTypeCase.IRRATIONAL, Real.RealTypeCase.INTEGER, Real.RealTypeCase.REALTYPE_NOT_SET, diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 7f8cafe175e..25b520d5a3e 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -92,7 +92,7 @@ fun Real.asWholeNumber(): Int? { RATIONAL -> if (rational.isOnlyWholeNumber()) rational.toWholeNumber() else null INTEGER -> integer IRRATIONAL -> null - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $this.") } } @@ -296,8 +296,7 @@ operator fun Real.div(rhs: Real): Real { * This function can fail in a few circumstances: * - One of the [Real]s is malformed or incomplete (such as a default instance). * - In cases where a root is being taken (i.e. when |[rhs]| < 1), if the root cannot be taken - * either an exception will be thrown or NaN will be returned (such as trying to take the even - * root of a negative value). + * either null or NaN will be returned (such as trying to take the even root of a negative value). * * Further, note that this function represents the real value root rather than the principal root, * so negative bases are allowed so long as the root being used is odd. For non-integerlike powers, @@ -326,7 +325,7 @@ operator fun Real.div(rhs: Real): Real { * (Note that the left column represents the left-hand side and the top row represents the * right-hand side of the operation). */ -infix fun Real.pow(rhs: Real): Real { +infix fun Real.pow(rhs: Real): Real? { // Powers can really only be effectively done via floats or whole-number only fractions. return when (realTypeCase) { RATIONAL -> { @@ -377,8 +376,7 @@ infix fun Real.pow(rhs: Real): Real { * Failure cases: * - An invalid [Real] is passed in (such as a default instance), resulting in an exception being * thrown. - * - A negative value is passed in (this will either result in an exception or a NaN being - * returned). + * - A negative value is passed in (this will either result in null or a NaN being returned). * * Similar to [Real.plus] & other operations, this function attempts to retain as much precision as * possible by first performing perfect roots before needing to perform a numerical approximation. @@ -394,7 +392,7 @@ infix fun Real.pow(rhs: Real): Real { * | irrational | irrational | irrational | * |------------------------------------------------| */ -fun sqrt(real: Real): Real { +fun sqrt(real: Real): Real? { return when (real.realTypeCase) { RATIONAL -> real.rational.root(base = 2, invert = false) IRRATIONAL -> createIrrationalReal(kotlin.math.sqrt(real.irrational)) @@ -439,7 +437,7 @@ private fun Int.pow(exp: Int): Real { } } -private fun Fraction.root(base: Int, invert: Boolean): Real { +private fun Fraction.root(base: Int, invert: Boolean): Real? { check(base > 0) { "Expected base of 1 or higher, not: $base" } val adjustedFraction = toImproperForm() @@ -448,24 +446,28 @@ private fun Fraction.root(base: Int, invert: Boolean): Real { val adjustedDenom = adjustedFraction.denominator val rootedNumerator = if (invert) root(adjustedDenom, base) else root(adjustedNum, base) val rootedDenominator = if (invert) root(adjustedNum, base) else root(adjustedDenom, base) - return if (rootedNumerator.isInteger() && rootedDenominator.isInteger()) { - Real.newBuilder().apply { - rational = Fraction.newBuilder().apply { - isNegative = rootedNumerator.isNegative() || rootedDenominator.isNegative() - numerator = rootedNumerator.integer.absoluteValue - denominator = rootedDenominator.integer.absoluteValue - }.build().toProperForm() - }.build() - } else { - // One or both of the components of the fraction can't be rooted, so compute an irrational - // version. - Real.newBuilder().apply { - irrational = rootedNumerator.toDouble() / rootedDenominator.toDouble() - }.build() + return when { + rootedNumerator == null || rootedDenominator == null -> null + rootedNumerator.isInteger() && rootedDenominator.isInteger() -> { + Real.newBuilder().apply { + rational = Fraction.newBuilder().apply { + isNegative = rootedNumerator.isNegative() || rootedDenominator.isNegative() + numerator = rootedNumerator.integer.absoluteValue + denominator = rootedDenominator.integer.absoluteValue + }.build().toProperForm() + }.build() + } + else -> { + // One or both of the components of the fraction can't be rooted, so compute an irrational + // version. + Real.newBuilder().apply { + irrational = rootedNumerator.toDouble() / rootedDenominator.toDouble() + }.build() + } } } -private fun root(int: Int, base: Int): Real { +private fun root(int: Int, base: Int): Real? { // First, check if the integer is a root. Base reference for possible methods: // https://www.researchgate.net/post/How-to-decide-if-a-given-number-will-have-integer-square-root-or-not. if (int == 0 && base == 0) { @@ -478,7 +480,7 @@ private fun root(int: Int, base: Int): Real { } check(base > 0) { "Expected base of 1 or higher, not: $base" } - check((int < 0 && base.isOdd()) || int >= 0) { "Radicand results in imaginary number: $int" } + if (int < 0 && !base.isOdd()) return null when { int == 0 -> { @@ -502,7 +504,7 @@ private fun root(int: Int, base: Int): Real { } val radicand = int.absoluteValue - var potentialRoot = base + var potentialRoot = 1 while (potentialRoot.pow(base).integer < radicand) { potentialRoot++ } 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 index 519cff70cf8..7a7aed1e686 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -98,6 +98,24 @@ oppia_android_test( ], ) +oppia_android_test( + name = "ExpressionToPolynomialConverterTest", + srcs = ["ExpressionToPolynomialConverterTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.ExpressionToPolynomialConverterTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", + "//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:math_expression_parser", + ], +) + oppia_android_test( name = "FloatExtensionsTest", srcs = ["FloatExtensionsTest.kt"], @@ -160,6 +178,7 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", "//testing/src/main/java/org/oppia/android/testing/math:real_subject", "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_truth", @@ -171,24 +190,6 @@ oppia_android_test( ], ) -oppia_android_test( - name = "ExpressionToPolynomialTest", - srcs = ["ExpressionToPolynomialTest.kt"], - custom_package = "org.oppia.android.util.math", - test_class = "org.oppia.android.util.math.ExpressionToPolynomialTest", - test_manifest = "//utility:test_manifest", - deps = [ - "//model/src/main/proto:math_java_proto_lite", - "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", - "//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:math_expression_parser", - ], -) - oppia_android_test( name = "MathExpressionParserTest", srcs = ["MathExpressionParserTest.kt"], @@ -293,8 +294,9 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", - "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialConverterTest.kt new file mode 100644 index 00000000000..84d4f434731 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialConverterTest.kt @@ -0,0 +1,2325 @@ +package org.oppia.android.util.math + +import com.google.common.truth.Truth.assertThat +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.MathExpression +import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat +import org.oppia.android.util.math.ExpressionToPolynomialConverter.Companion.reduceToPolynomial +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.REQUIRED_ONLY +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.robolectric.annotation.LooperMode + +/** + * Tests for [ExpressionToPolynomialConverter]. + * + * Note that this suite only tests with algebraic expressions since numeric expressions are never + * considered to be polynomials (despite numeric expression evaluation and the constant term of + * polynomials being expected to always result in the same value). + */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class ExpressionToPolynomialConverterTest { + @Test + fun testReduce_integerConstantExpression_returnsConstantPolynomial() { + val expression = parseAlgebraicExpression("2") + + val polynomial = expression.reduceToPolynomial() + + assertThat(polynomial).isConstantThat().isIntegerThat().isEqualTo(2) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("2") + } + + @Test + fun testReduce_decimalConstantExpression_returnsConstantPolynomial() { + val expression = parseAlgebraicExpression("3.14") + + val polynomial = expression.reduceToPolynomial() + + assertThat(polynomial).isConstantThat().isIrrationalThat().isWithin(1e-5).of(3.14) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("3.14") + } + + @Test + fun testReduce_variableConstantExpression_returnsSingleTermPolynomial() { + val expression = parseAlgebraicExpression("x") + + val polynomial = expression.reduceToPolynomial() + + assertThat(polynomial).hasTermCountThat().isEqualTo(1) + assertThat(polynomial).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(polynomial).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(polynomial).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(polynomial).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x") + } + + @Test + fun testReduce_intTimesVariable_returnsPolynomialWithCoefficient() { + val expression = parseAlgebraicExpression("7*x") + + val polynomial = expression.reduceToPolynomial() + + assertThat(polynomial).hasTermCountThat().isEqualTo(1) + assertThat(polynomial).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(7) + assertThat(polynomial).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(polynomial).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(polynomial).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("7x") + } + + @Test + fun testReduce_negativeDecimalTimesVariable_returnsPolynomialWithNegativeCoefficient() { + val expression = parseAlgebraicExpression("-3.14*x") + + val polynomial = expression.reduceToPolynomial() + + assertThat(polynomial).hasTermCountThat().isEqualTo(1) + assertThat(polynomial).term(0).hasCoefficientThat().isIrrationalThat().isWithin(1e-5).of(-3.14) + assertThat(polynomial).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(polynomial).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(polynomial).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("-3.14x") + } + + @Test + fun testReduce_twoTimesXImplicitly_returnsPolynomialWithOneTermAndCoefficient() { + val expression = parseAlgebraicExpression("2x") + + val polynomial = expression.reduceToPolynomial() + + assertThat(polynomial).hasTermCountThat().isEqualTo(1) + assertThat(polynomial).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(2) + assertThat(polynomial).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(polynomial).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(polynomial).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("2x") + } + + @Test + fun testReduce_zeroX_returnsZeroPolynomial() { + val expression = parseAlgebraicExpression("0x") + + val polynomial = expression.reduceToPolynomial() + + // 0x just becomes 0 (the 'x' is removed). + assertThat(polynomial).isConstantThat().isIntegerThat().isEqualTo(0) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("0") + } + + @Test + fun testReduce_onePlusTwo_returnsConstantThreePolynomial() { + val expression = parseAlgebraicExpression("1+2") + + val polynomial = expression.reduceToPolynomial() + + // The '1+2' is reduced to a single '3' constant. + assertThat(polynomial).isConstantThat().isIntegerThat().isEqualTo(3) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("3") + } + + @Test + fun testReduce_xPlusX_returnTwoXPolynomial() { + val expression = parseAlgebraicExpression("x+x") + + val polynomial = expression.reduceToPolynomial() + + // x+x is combined to 2x (like terms are combined). + assertThat(polynomial).hasTermCountThat().isEqualTo(1) + assertThat(polynomial).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(2) + assertThat(polynomial).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(polynomial).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(polynomial).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("2x") + } + + @Test + fun testReduce_xPlusOne_returnsXPlusOnePolynomial() { + val expression = parseAlgebraicExpression("x+1") + + val polynomial = expression.reduceToPolynomial() + + // x+1 leads to a two-term polynomial. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x + 1") + } + + @Test + fun testReduce_onePlusX_returnsXPlusOnePolynomial() { + val expression = parseAlgebraicExpression("1+x") + + val polynomial = expression.reduceToPolynomial() + + // 1+x leads to a two-term polynomial (with 'x' sorted first). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x + 1") + } + + @Test + fun testReduce_xMinusOne_returnsXMinusOnePolynomial() { + val expression = parseAlgebraicExpression("x-1") + + val polynomial = expression.reduceToPolynomial() + + // x-1 leads to a two-term polynomial. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x - 1") + } + + @Test + fun testReduce_oneMinusX_returnsNegativeXPlusOne() { + val expression = parseAlgebraicExpression("1-x") + + val polynomial = expression.reduceToPolynomial() + + // 1-x leads to a two-term polynomial (note that 'x' is listed first due to sort priority). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("-x + 1") + } + + @Test + fun testReduce_xPlusTwoX_returnsThreeXPolynomial() { + val expression = parseAlgebraicExpression("x+2x") + + val polynomial = expression.reduceToPolynomial() + + // x+2x combines to 3x (since like terms are combined). This also verifies that coefficients are + // correctly combined. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("3x") + } + + @Test + fun testReduce_xYPlusYzMinusXzMinusYzPlusThreeXy_returnsFourXyMinusXzPolynomial() { + val expression = parseAlgebraicExpression("xy+yz-xz-yz+3xy") + + val polynomial = expression.reduceToPolynomial() + + // xy+yz-xz-yz+3xy combines to 4xy-xz (eliminated terms are removed). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(4) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("z") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("4xy - xz") + } + + @Test + fun testReduce_xy_returnsXTimesYPolynomial() { + val expression = parseAlgebraicExpression("xy") + + val polynomial = expression.reduceToPolynomial() + + // xy is a single-term, two-variable polynomial. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("xy") + } + + @Test + fun testReduce_four_timesXPlusTwo_returnsEightXPlusEightPolynomial() { + val expression = parseAlgebraicExpression("4*(x+2)") + + val polynomial = expression.reduceToPolynomial() + + // 4*(x+2) becomes 4x+8 (the constant distributes to each term's coefficient). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(4) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(8) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("4x + 8") + } + + @Test + fun testReduce_x_timesOnePlusX_returnsXSquaredPlusXPolynomial() { + val expression = parseAlgebraicExpression("x(1+x)") + + val polynomial = expression.reduceToPolynomial() + + // x(1+x) is expanded to x^2+x. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2 + x") + } + + @Test + fun testReduce_y_timesOnePlusX_returnsXyPlusYPolynomial() { + val expression = parseAlgebraicExpression("y(1+x)") + + val polynomial = expression.reduceToPolynomial() + + // y(1+x) is expanded to xy+y. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("xy + y") + } + + @Test + fun testReduce_xPlusOne_timesXMinusOne_returnsXSquaredMinusOnePolynomial() { + val expression = parseAlgebraicExpression("(x+1)(x-1)") + + val polynomial = expression.reduceToPolynomial() + + // (x+1)(x-1) expands to x^2-1. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2 - 1") + } + + @Test + fun testReduce_xMinusOne_timesXPlusOne_returnsXSquaredMinusOnePolynomial() { + val expression = parseAlgebraicExpression("(x-1)(x+1)") + + val polynomial = expression.reduceToPolynomial() + + // (x-1)(x+1) expands to x^2-1 (demonstrating multiplication commutativity). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2 - 1") + } + + @Test + fun testReduce_xPlusOne_timesXPlusOne_returnsXSquaredPlusTwoXPlusOnePolynomial() { + val expression = parseAlgebraicExpression("(x+1)(x+1)") + + val polynomial = expression.reduceToPolynomial() + + // (x+1)(x+1) expands to x^2+2x+1 (binomial multiplication). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(3) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2 + 2x + 1") + } + + @Test + fun testReduce_twoMinusX_timesThreeXPlusSeven_returnsMinusThreeXSqPlusXPlusFourteenPolynomial() { + val expression = parseAlgebraicExpression("(2-x)(3x+7)") + + val polynomial = expression.reduceToPolynomial() + + // (2-x)(3x+7) expands to -3x^2-x+14 (shows multiplication with x coefficients). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(3) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(14) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("-3x^2 - x + 14") + } + + @Test + fun testReduce_xRaisedToTwo_returnsXSquaredPolynomial() { + val expression = parseAlgebraicExpression("x^2") + + val polynomial = expression.reduceToPolynomial() + + // x^2 is treated as the variable 'x' with power 2. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2") + } + + @Test + fun testReduce_xSquaredPlusXPlusOne_returnsXSquaredPlusXPlusOnePolynomial() { + val expression = parseAlgebraicExpression("x^2+x+1") + + val polynomial = expression.reduceToPolynomial() + + // x^2+x+1 stays the same since no terms can be combined, eliminated, or reordered. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(3) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2 + x + 1") + } + + @Test + fun testReduce_xSquaredPlusXPlusXY_returnsXSquaredPlusXyPlusXPolynomial() { + val expression = parseAlgebraicExpression("x^2+x+xy") + + val polynomial = expression.reduceToPolynomial() + + // x^2+x+xy is treated as the same polynomial, though 'xy' comes before 'x' per sorting rules. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(3) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2 + xy + x") + } + + @Test + fun testReduce_x_timesXSquared_returnsXCubedPolynomial() { + val expression = parseAlgebraicExpression("xx^2") + + val polynomial = expression.reduceToPolynomial() + + // xx^2 becomes x^3 since like terms are multiplied and simplified. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^3") + } + + @Test + fun testReduce_xSquared_plusXSquaredY_returnsXSquaredYPlusXSquared() { + val expression = parseAlgebraicExpression("x^2 + x^2y") + + val polynomial = expression.reduceToPolynomial() + + // x^2+x^2y becomes x^2y+x^2 (terms reordered, but nothing should be combined). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2y + x^2") + } + + @Test + fun testReduce_constant_division_returnsFractionalPolynomial() { + val expression = parseAlgebraicExpression("1/2") + + val polynomial = expression.reduceToPolynomial() + + // Division of constants is actually computed. + assertThat(polynomial).isConstantThat().isEqualTo(ONE_HALF) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("1/2") + } + + @Test + fun testReduce_decimalConstant_division_returnsIrrationalPolynomial() { + val expression = parseAlgebraicExpression("3.14/2") + + val polynomial = expression.reduceToPolynomial() + + // Division of constants is actually computed. + assertThat(polynomial).isConstantThat().isIrrationalThat().isWithin(1e-5).of(1.57) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("1.57") + } + + @Test + fun testReduce_x_dividedByZero_returnsNullPolynomial() { + // Dividing by zero is an optional error that needs to be disabled for this check. + val expression = parseAlgebraicExpression("x/0", errorCheckingMode = REQUIRED_ONLY) + + val polynomial = expression.reduceToPolynomial() + + // Cannot divide by zero. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_x_dividedByOneMinusOne_returnsNullPolynomial() { + val expression = parseAlgebraicExpression("x/(1-1)") + + val polynomial = expression.reduceToPolynomial() + + // Cannot divide by zero, even in cases when the denominator needs to be evaluated. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_x_dividedByXMinusX_returnsNullPolynomial() { + val expression = parseAlgebraicExpression("x/(x-x)") + + val polynomial = expression.reduceToPolynomial() + + // Another division by zero, but more complex. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_two_dividedByZero_returnsNullPolynomial() { + // Dividing by zero is an optional error that needs to be disabled for this check. + val expression = parseAlgebraicExpression("2/0", errorCheckingMode = REQUIRED_ONLY) + + val polynomial = expression.reduceToPolynomial() + + // Division by zero is not allowed for purely constant polynomials, either. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_x_dividedByTwo_returnsOneHalfXPolynomial() { + val expression = parseAlgebraicExpression("x/2") + + val polynomial = expression.reduceToPolynomial() + + // x/2 is treated as (1/2)x (that is, the variable 'x' with coefficient '1/2'). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isEqualTo(ONE_HALF) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("(1/2)x") + } + + @Test + fun testReduce_one_dividedByX_returnsNullPolynomial() { + val expression = parseAlgebraicExpression("1/x") + + val polynomial = expression.reduceToPolynomial() + + // Polynomials cannot have negative powers, so dividing by a polynomial isn't valid. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_x_dividedByX_returnsOnePolynomial() { + val expression = parseAlgebraicExpression("x/x") + + val polynomial = expression.reduceToPolynomial() + + // x/x is just '1'. + assertThat(polynomial).isConstantThat().isIntegerThat().isEqualTo(1) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("1") + } + + @Test + fun testReduce_x_dividedByNegativeTwo_returnsNegativeOneHalfXPolynomial() { + val expression = parseAlgebraicExpression("x/-2") + + val polynomial = expression.reduceToPolynomial() + + // x/-2 is treated as (-1/2)x (that is, the variable 'x' with coefficient '-1/2'). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isEqualTo(-ONE_HALF) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("(-1/2)x") + } + + @Test + fun testReduce_xPlusOne_dividedByTwo_returnsOneHalfXPlusOneHalfPolynomial() { + val expression = parseAlgebraicExpression("(x+1)/2") + + val polynomial = expression.reduceToPolynomial() + + // (x+1)/2 expands to (1/2)x+(1/2), a two-term polynomial. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isEqualTo(ONE_HALF) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isEqualTo(ONE_HALF) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("(1/2)x + 1/2") + } + + @Test + fun testReduce_xSquaredPlusX_dividedByX_returnsXPlusOnePolynomial() { + val expression = parseAlgebraicExpression("(x^2+x)/x") + + val polynomial = expression.reduceToPolynomial() + + // (x^2+x)/x becomes x+1 ('x' is factored out). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x + 1") + } + + @Test + fun testReduce_xyPlusY_dividedByX_returnsNullPolynomial() { + val expression = parseAlgebraicExpression("(xy+y)/x") + + val polynomial = expression.reduceToPolynomial() + + // 'x' cannot be fully factored out of 'xy+y'. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_xyPlusY_dividedByY_returnsXPlusOnePolynomial() { + val expression = parseAlgebraicExpression("(xy+y)/y") + + val polynomial = expression.reduceToPolynomial() + + // (xy+y)/y becomes x+1 ('y' is factored out). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x + 1") + } + + @Test + fun testReduce_xyPlusY_dividedByXy_returnsNullPolynomial() { + val expression = parseAlgebraicExpression("(xy+y)/(xy)") + + val polynomial = expression.reduceToPolynomial() + + // 'xy' cannot be cleanly factored out of 'xy+y'. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_xYMinusFiveY_dividedByY_returnsXMinusFivePolynomial() { + val expression = parseAlgebraicExpression("(xy-5y)/y") + + val polynomial = expression.reduceToPolynomial() + + // (xy-5y)/y becomes x-5 (demonstrates that variables become coefficients in such cases). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-5) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x - 5") + } + + @Test + fun testReduce_xSquaredMinusOne_dividedByXPlusOne_returnsXMinusOnePolynomial() { + val expression = parseAlgebraicExpression("(x^2-1)/(x+1)") + + val polynomial = expression.reduceToPolynomial() + + // (x^2-1)/(x+1)=x-1. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x - 1") + } + + @Test + fun testReduce_xSquaredMinusOne_dividedByXMinusOne_returnsXPlusOnePolynomial() { + val expression = parseAlgebraicExpression("(x^2-1)/(x-1)") + + val polynomial = expression.reduceToPolynomial() + + // (x^2-1)/(x-1)=x+1. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x + 1") + } + + @Test + fun testReduce_xSquaredPlusTwoXPlusOne_dividedByXPlusOne_returnsXPlusOnePolynomial() { + val expression = parseAlgebraicExpression("(x^2+2x+1)/(x+1)") + + val polynomial = expression.reduceToPolynomial() + + // (x^2+2x+1)/(x+1)=x+1. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x + 1") + } + + @Test + fun testReduce_negThreeXSqAddTwentyThreeXSubFourteen_dividedBySevenSubX_retsThreeXSubTwoPoly() { + val expression = parseAlgebraicExpression("(-3x^2+23x-14)/(7-x)") + + val polynomial = expression.reduceToPolynomial() + + // (-3x^2+23x-14)/(7-x)=3x-2 (demonstrates both deriving a non-one coefficient in the quotient, + // and dividing with negative leading terms). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-2) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("3x - 2") + } + + @Test + fun testReduce_xSquaredMinusTwoXyPlusYSquared_dividedByXMinusY_returnsXMinusYPolynomial() { + val expression = parseAlgebraicExpression("(x^2-2xy+y^2)/(x-y)") + + val polynomial = expression.reduceToPolynomial() + + // (x^2-2xy+y^2)/(x-y)=x-y (demonstrates factoring out both 'x' and 'y' terms). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x - y") + } + + @Test + fun testReduce_xCubedMinusYCubed_dividedByXMinusY_returnsXSquaredPlusXyPlusYSquaredPolynomial() { + val expression = parseAlgebraicExpression("(x^3-y^3)/(x-y)") + + val polynomial = expression.reduceToPolynomial() + + // (x^3-y^3)/(x-y)=x^2+xy+y^2. This demonstrates a more complex case where a new term can appear + // due to the division. This example comes from: + // https://www.kristakingmath.com/blog/predator-prey-systems-ghtcp-5e2r4-427ab. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(3) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2 + xy + y^2") + } + + @Test + fun testReduce_xCubedMinusThreeXSqYPlusXySqMinusYCubed_dividedByXMinusYSq_retsXMinusYPoly() { + val expression = parseAlgebraicExpression("(x^3-3x^2y+3xy^2-y^3)/(x-y)^2") + + val polynomial = expression.reduceToPolynomial() + + // (x^3-3x^2y+3xy^2-y^3)/(x-y)^2=x-y (demonstrates dividing a variable term with a power larger + // than 1). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x - y") + } + + @Test + fun testReduce_zeroRaisedToZero_returnsOne() { + val expression = parseAlgebraicExpression("0^0") + + val polynomial = expression.reduceToPolynomial() + + // 0^0=1 (for consistency with other 'pow' functions). + assertThat(polynomial).isConstantThat().isIntegerThat().isEqualTo(1) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("1") + } + + @Test + fun testReduce_zeroRaisedToOne_returnsZero() { + val expression = parseAlgebraicExpression("0^1") + + val polynomial = expression.reduceToPolynomial() + + // 0^1=0. + assertThat(polynomial).isConstantThat().isIntegerThat().isEqualTo(0) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("0") + } + + @Test + fun testReduce_oneRaisedToZero_returnsOne() { + val expression = parseAlgebraicExpression("1^0") + + val polynomial = expression.reduceToPolynomial() + + // 1^0=1. + assertThat(polynomial).isConstantThat().isIntegerThat().isEqualTo(1) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("1") + } + + @Test + fun testReduce_twoRaisedToZero_returnsOne() { + val expression = parseAlgebraicExpression("2^0") + + val polynomial = expression.reduceToPolynomial() + + // 2^0=1. + assertThat(polynomial).isConstantThat().isIntegerThat().isEqualTo(1) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("1") + } + + @Test + fun testReduce_xRaisedToZero_returnsOnePolynomial() { + val expression = parseAlgebraicExpression("x^0") + + val polynomial = expression.reduceToPolynomial() + + // x^0 is just 1 since anything raised to '1' is 1. + assertThat(polynomial).isConstantThat().isIntegerThat().isEqualTo(1) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("1") + } + + @Test + fun testReduce_xRaisedToOne_returnsXPolynomial() { + val expression = parseAlgebraicExpression("x^1") + + val polynomial = expression.reduceToPolynomial() + + // x^1 is just 'x' (i.e. a polynomial with a variable term 'x' with power '1'). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x") + } + + @Test + fun testReduce_xRaisedToNegativeOne_returnsNullPolynomial() { + val expression = parseAlgebraicExpression("x^-1") + + val polynomial = expression.reduceToPolynomial() + + // Polynomials cannot have negative powers. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_twoRaisedToQuantityThreeMinusSix_returnsOneEighthPolynomial() { + val expression = parseAlgebraicExpression("2^(3-6)") + + val polynomial = expression.reduceToPolynomial() + + // 2^(3-6) evaluates to 1/8 (i.e. constants can be raised to negative powers). + assertThat(polynomial).isConstantThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(8) + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("1/8") + } + + @Test + fun testReduce_xRaisedToQuantityThreeMinusSix_returnsNullPolynomial() { + val expression = parseAlgebraicExpression("x^(3-6)") + + val polynomial = expression.reduceToPolynomial() + + // Polynomials cannot have negative powers. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_negativeTwoXQuantityRaisedToTwo_returnsFourXSquaredPolynomial() { + val expression = parseAlgebraicExpression("(-2x)^2") + + val polynomial = expression.reduceToPolynomial() + + // (-2x)^2=4x^2 (negative term goes away and coefficient is multiplied). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(4) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("4x^2") + } + + @Test + fun testReduce_negativeTwoXQuantityRaisedToThree_returnsNegativeEightXCubedPolynomial() { + val expression = parseAlgebraicExpression("(-2x)^3") + + val polynomial = expression.reduceToPolynomial() + + // (-2x)^3=-8x^3 (the negative is kept due to an odd power. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-8) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("-8x^3") + } + + @Test + fun testReduce_xYRaisedToTwo_returnsXYSquaredPolynomial() { + val expression = parseAlgebraicExpression("xy^2") + + val polynomial = expression.reduceToPolynomial() + + // For 'xy^2' the 'y' will have power '2' and 'x' will have power '1'. This and related tests + // help to verify that exponentiation assigns the power to the correct variable when parsing + // polynomial syntax. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("xy^2") + } + + @Test + fun testReduce_yXRaisedToTwo_returnsXSquaredYPolynomial() { + val expression = parseAlgebraicExpression("yx^2") + + val polynomial = expression.reduceToPolynomial() + + // For 'x^2y' the 'x' will have power '2' and 'y' will have power '1'. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2y") + } + + @Test + fun testReduce_xRaisedToTwoYRaisedToTwo_returnsXSquaredYSquaredPolynomial() { + val expression = parseAlgebraicExpression("x^2y^2") + + val polynomial = expression.reduceToPolynomial() + + // For 'x^2y^2' both variables have power '2'. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2y^2") + } + + @Test + fun testReduce_twoRaisedToX_returnsNullPolynomial() { + // Raising to a variable term is an optional error that needs to be disabled for this check. + val expression = parseAlgebraicExpression("2^x", errorCheckingMode = REQUIRED_ONLY) + + val polynomial = expression.reduceToPolynomial() + + // 2^x is not a polynomial since powers must be positive integers. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_xRaisedToX_returnsNullPolynomial() { + // Raising to a variable term is an optional error that needs to be disabled for this check. + val expression = parseAlgebraicExpression("x^x", errorCheckingMode = REQUIRED_ONLY) + + val polynomial = expression.reduceToPolynomial() + + // x^x is not a polynomial since powers must be positive integers. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_squareRootX_returnsNullPolynomial() { + val expression = parseAlgebraicExpression("sqrt(x)") + + val polynomial = expression.reduceToPolynomial() + + // sqrt(x) is not a polynomial. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_squareRootXQuantitySquared_returnsXPolynomial() { + val expression = parseAlgebraicExpression("sqrt(x)^2") + + val polynomial = expression.reduceToPolynomial() + + // This doesn't currently evaluate correctly due to a limitation in the reduction algorithm (it + // can't represent sub-polynomials with fractional powers). + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_squareRootOfXSquared_returnsXPolynomial() { + val expression = parseAlgebraicExpression("sqrt(x^2)") + + val polynomial = expression.reduceToPolynomial() + + // sqrt(x^2) is simplified to 'x'. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x") + } + + @Test + fun testReduce_squareRootOfOnePlusX_returnsNullPolynomial() { + val expression = parseAlgebraicExpression("sqrt(1+x)") + + val polynomial = expression.reduceToPolynomial() + + // 1+x has no square root. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_squareRootOfFourXSquared_returnsTwoXPolynomial() { + val expression = parseAlgebraicExpression("sqrt(4x^2)") + + val polynomial = expression.reduceToPolynomial() + + // sqrt(4x^2)=2x. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("2x") + } + + @Test + fun testReduce_squareRootOfNegativeFourXSquared_returnsNullPolynomial() { + val expression = parseAlgebraicExpression("sqrt(-4x^2)") + + val polynomial = expression.reduceToPolynomial() + + // sqrt(-4x^2) is not valid since negative even roots result in imaginary results. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_squareRootOfXSquaredYSquared_returnsXyPolynomial() { + val expression = parseAlgebraicExpression("√(x^2y^2)") + + val polynomial = expression.reduceToPolynomial() + + // √(x^2y^2) evaluates to xy (i.e. individual variable terms can be extracted and rooted). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("xy") + } + + @Test + fun testReduce_squareTwoXSquared_returnsIrrationalCoefficientXPolynomial() { + val expression = parseAlgebraicExpression("√(2x^2)") + + val polynomial = expression.reduceToPolynomial() + + // √(2x^2) evaluates to a polynomial with a decimal coefficient. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIrrationalThat().isWithin(1e-5).of(1.414213562) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().matches("1.414\\d+x") + } + + @Test + fun testReduce_sixteenXToTheFourth_raisedToOneFourth_returnsTwoXPolynomial() { + val expression = parseAlgebraicExpression("((2x)^4)^(1/4)") + + val polynomial = expression.reduceToPolynomial() + + // ((2x)^4)^(1/4)=2x (demonstrates root-based operations with exponentiation). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("2x") + } + + @Test + fun testReduce_negativeSixteenXToTheFourth_raisedToOneFourth_returnsNullPolynomial() { + val expression = parseAlgebraicExpression("(-16x^4)^(1/4)") + + val polynomial = expression.reduceToPolynomial() + + // (-16x^4)^(1/4) is not valid since negative even roots result in imaginary results. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_negativeTwentySevenYCubed_raisedToOneThird_returnsNegativeThreeXPolynomial() { + val expression = parseAlgebraicExpression("(-27y^3)^(1/3)") + + val polynomial = expression.reduceToPolynomial() + + // (-27y^3)^(1/3)=-3y (shows that odd roots can accept real-valued negative radicands). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("-3y") + } + + @Test + fun testReduce_xSquared_raisedToOneHalf_returnsXPolynomial() { + val expression = parseAlgebraicExpression("(x^2)^(1/2)") + + val polynomial = expression.reduceToPolynomial() + + // (x^2)^(1/2) simplifies to just 'x'. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x") + } + + @Test + fun testReduce_xToTheOneHalf_squared_returnsXPolynomial() { + val expression = parseAlgebraicExpression("(x^(1/2))^2") + + val polynomial = expression.reduceToPolynomial() + + // This doesn't currently evaluate correctly due to a limitation in the reduction algorithm (it + // can't represent sub-polynomials with fractional powers). + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_xCubed_raisedToOneThird_returnsXPolynomial() { + val expression = parseAlgebraicExpression("(x^3)^(1/3)") + + val polynomial = expression.reduceToPolynomial() + + // (x^3)^(1/3) simplifies to just 'x'. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x") + } + + @Test + fun testReduce_xCubed_raisedToTwoThirds_returnsXSquaredPolynomial() { + val expression = parseAlgebraicExpression("(x^3)^(2/3)") + + val polynomial = expression.reduceToPolynomial() + + // (x^3)^(2/3) simplifies to 'x^2'. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2") + } + + @Test + fun testReduce_xToTheOneThird_cubed_returnsXPolynomial() { + val expression = parseAlgebraicExpression("(x^(1/3))^3") + + val polynomial = expression.reduceToPolynomial() + + // This doesn't currently evaluate correctly due to a limitation in the reduction algorithm (it + // can't represent sub-polynomials with fractional powers). + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_xPlusOne_squared_returnsXSquaredPlusTwoXPlusOnePolynomial() { + val expression = parseAlgebraicExpression("(x+1)^2") + + val polynomial = expression.reduceToPolynomial() + + // (x+1)^2=x^2+2x+1 (simple binomial multiplication). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(3) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2 + 2x + 1") + } + + @Test + fun testReduce_xPlusOne_cubed_returnsXCubedPlusThreeXSquaredPlusThreeXPlusOnePolynomial() { + val expression = parseAlgebraicExpression("(x+1)^3") + + val polynomial = expression.reduceToPolynomial() + + // (x+1)^3=x^3+3x^2+3x+1 (simple binomial multiplication per Pascal's triangle). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(4) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^3 + 3x^2 + 3x + 1") + } + + @Test + fun testReduce_xMinusYCubed_returnsXCubedMinusThreeXSqYPlusThreeXYSqMinusYCubedPolynomial() { + val expression = parseAlgebraicExpression("(x-y)^3") + + val polynomial = expression.reduceToPolynomial() + + // (x-y)^3=x^3-3x^2y+3xy^2-y^3 (show that exponentiation works with double variable terms, too). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(4) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(3) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^3 - 3x^2y + 3xy^2 - y^3") + } + + @Test + fun testReduce_xSquaredPlusTwoXPlusOne_raisedToOneHalf_returnsNullPolynomial() { + val expression = parseAlgebraicExpression("(x^2+2x+1)^(1/2)") + + val polynomial = expression.reduceToPolynomial() + + // While (x^2+2x+1)^(1/2) can technically be factored to (x+1), the system doesn't yet support + // factoring polynomials via roots. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_xRaisedToTwoPlusTwo_returnsXToTheFourthPolynomial() { + val expression = parseAlgebraicExpression("x^(2+2)") + + val polynomial = expression.reduceToPolynomial() + + // x^(2+2)=x^4 (the exponent is evaluated). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(4) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^4") + } + + @Test + fun testReduce_xRaisedToTwoMinusTwo_returnsOnePolynomial() { + val expression = parseAlgebraicExpression("x^(2-2)") + + val polynomial = expression.reduceToPolynomial() + + // x^(2-2)=1 (since 2-2 evaluates to 0, and x^0 is 1). + assertThat(polynomial).isConstantThat().isIntegerThat().isEqualTo(1) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("1") + } + + @Test + fun testReduce_moreComplexArithmeticExpression_returnsCorrectlyComputedCoefficientsPolynomial() { + val expression = parseAlgebraicExpression("133+3.14*x/(11-15)^2") + + val polynomial = expression.reduceToPolynomial() + + // 133+3.14*x/(11-15)^2 simplifies to 0.19625x+133. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIrrationalThat().isWithin(1e-5).of(0.19625) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(133) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("0.19625x + 133") + } + + /* + * Tests to verify that ordering matches https://en.wikipedia.org/wiki/Polynomial#Definition + * (where multiple variables are sorted lexicographically). + */ + + @Test + fun testReduce_xCubedPlusXSquaredPlusXPlusOne_returnsSameOrderPolynomial() { + val expression = parseAlgebraicExpression("x^3+x^2+x+1") + + val polynomial = expression.reduceToPolynomial() + + // x^3+x^2+x+1 retains its order since higher power terms are ordered first. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(4) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^3 + x^2 + x + 1") + } + + @Test + fun testReduce_onePlusXPlusXSquaredPlusXCubed_returnsXCubedPlusXSquaredPlusXPlusOnePolynomial() { + val expression = parseAlgebraicExpression("1+x+x^2+x^3") + + val polynomial = expression.reduceToPolynomial() + + // 1+x+x^2+x^3 is reversed to x^3+x^2+x+1 since higher power terms are ordered first. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(4) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^3 + x^2 + x + 1") + } + + @Test + fun testReduce_xyPlusXzPlusYz_returnsSameOrderPolynomial() { + val expression = parseAlgebraicExpression("xy+xz+yz") + + val polynomial = expression.reduceToPolynomial() + + // xy+xz+yz retains its order since multivariable terms are ordered lexicographically. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(3) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("z") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("z") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("xy + xz + yz") + } + + @Test + fun testReduce_zYPlusZxPlusYX_returnsXyPlusXzPlusYzPolynomial() { + val expression = parseAlgebraicExpression("zy+zx+yx") + + val polynomial = expression.reduceToPolynomial() + + // zy+zx+yx is reversed in ordered and terms to be xy+xz+yz since multivariable terms are + // ordered lexicographically. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(3) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("z") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("z") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("xy + xz + yz") + } + + @Test + fun testReduce_complexMultiVariableOutOfOrderExpression_returnsCorrectlyOrderedPolynomial() { + val expression = parseAlgebraicExpression("3+y+x+yx+x^2y+x^2y^2+y^2x") + + val polynomial = expression.reduceToPolynomial() + + // 3+y+x+yx+x^2y+x^2y^2+y^2x is sorted to: x^2y^2+x^2y+xy^2+xy+x+y+3 per term sorting rules. + // ordered lexicographically. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(7) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(4).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(5).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(6).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial) + .evaluatesToPlainTextThat() + .isEqualTo("x^2y^2 + x^2y + xy^2 + xy + x + y + 3") + } + + @Test + fun testEquals_twoPolynomial_twoPolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("2") + val polynomial2 = parsePolynomialFromAlgebraicExpression("2") + + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_zeroPolynomial_negativeZeroPolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("0") + val polynomial2 = parsePolynomialFromAlgebraicExpression("-0") + + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_twoPolynomial_negativeTwoPolynomial_areNotEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("2") + val polynomial2 = parsePolynomialFromAlgebraicExpression("-2") + + assertThat(polynomial1).isNotEqualTo(polynomial2) + } + + @Test + fun testEquals_onePlusTwoPolynomial_threePolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("1+2") + val polynomial2 = parsePolynomialFromAlgebraicExpression("3") + + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_threePolynomial_onePlusTwoPolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("3") + val polynomial2 = parsePolynomialFromAlgebraicExpression("1+2") + + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_oneMinusTwoPolynomial_negativeOnePolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("1-2") + val polynomial2 = parsePolynomialFromAlgebraicExpression("-1") + + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_twoTimesSixPolynomial_sixPolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("2*3") + val polynomial2 = parsePolynomialFromAlgebraicExpression("6") + + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_twoRaisedToThreePolynomial_eightPolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("2^3") + val polynomial2 = parsePolynomialFromAlgebraicExpression("8") + + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_xPolynomial_xPolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("x") + val polynomial2 = parsePolynomialFromAlgebraicExpression("x") + + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_xPolynomial_twoPolynomial_areNotEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("x") + val polynomial2 = parsePolynomialFromAlgebraicExpression("2") + + assertThat(polynomial1).isNotEqualTo(polynomial2) + } + + @Test + fun testEquals_onePlusXPolynomial_xPlusOnePolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("1+x") + val polynomial2 = parsePolynomialFromAlgebraicExpression("x+1") + + // Demonstrate that commutativity doesn't matter (for addition). + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_xPlusYPolynomial_yPlusXPolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("x+y") + val polynomial2 = parsePolynomialFromAlgebraicExpression("y+x") + + // Commutativity doesn't change for variable ordering. + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_oneMinusXPolynomial_xMinusOnePolynomial_areNotEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("1-x") + val polynomial2 = parsePolynomialFromAlgebraicExpression("x-1") + + // Subtraction is not commutative. + assertThat(polynomial1).isNotEqualTo(polynomial2) + } + + @Test + fun testEquals_twoXPolynomial_xTimesTwoPolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("2x") + val polynomial2 = parsePolynomialFromAlgebraicExpression("x*2") + + // Demonstrate that commutativity doesn't matter (for multiplication). + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_twoDividedByXPolynomial_xDividedByTwoPolynomial_areNotEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("2/x") + val polynomial2 = parsePolynomialFromAlgebraicExpression("x/2") + + // Division is not commutative. + assertThat(polynomial1).isNotEqualTo(polynomial2) + } + + @Test + fun testEquals_xTimesQuantityXPlusOnePolynomial_xSquaredPlusXPolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("x(x+1)") + val polynomial2 = parsePolynomialFromAlgebraicExpression("x^2+x") + + // Multiplication is expanded. + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_threeXSubTwoTimesSevenSubX_minusThreeXSqAddTwentyThreeXSubFourteen_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("(3x-2)(7-x)") + val polynomial2 = parsePolynomialFromAlgebraicExpression("-3x^2+23x-14") + + // Multiplication is expanded. + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_quantityXPlusOneSquaredPolynomial_xSquaredPlusTwoXPlusOnePolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("(x+1)^2") + val polynomial2 = parsePolynomialFromAlgebraicExpression("x^2+2x+1") + + // Exponentiation is expanded. + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_quantityXPlusOneDividedByTwoPolynomial_oneHalfXPlusOneHalfPolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("(x+1)/2") + val polynomial2 = parsePolynomialFromAlgebraicExpression("x/2+(1/2)") + + // Division distributes. + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_squareRootOnePlusOnePolynomial_squareRootTwoPolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("sqrt(1+1)") + val polynomial2 = parsePolynomialFromAlgebraicExpression("sqrt(2)") + + // The two are equal after evaluation (to contrast with comparable operations). + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_squareRootTwoPolynomial_squareRootThreePolynomial_areNotEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("sqrt(2)") + val polynomial2 = parsePolynomialFromAlgebraicExpression("sqrt(3)") + + // The evaluated constants are actually different. + assertThat(polynomial1).isNotEqualTo(polynomial2) + } + + @Test + fun testEquals_squareRootTwoXSquaredPolynomial_twoXSquaredToOneHalfPolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("sqrt(2x^2)") + val polynomial2 = parsePolynomialFromAlgebraicExpression("(2x^2)^(1/2)") + + // sqrt() is the same as raising to 1/2. + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_complexPolynomial_samePolynomialInDifferentOrder_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("3+y+x+yx+x^2y+x^2y^2+y^2x") + val polynomial2 = parsePolynomialFromAlgebraicExpression("xy+xy^2+x^2y+y^2x^2+3+x+y") + + // Order doesn't matter. + assertThat(polynomial1).isEqualTo(polynomial2) + } + + private fun parsePolynomialFromAlgebraicExpression(expression: String) = + parseAlgebraicExpression(expression).reduceToPolynomial() + + private companion object { + private fun parseAlgebraicExpression( + expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + ): MathExpression { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables = listOf("x", "y", "z"), errorCheckingMode + ).getExpectedSuccess() + } + + private inline fun MathParsingResult.getExpectedSuccess(): T { + assertThat(this).isInstanceOf(MathParsingResult.Success::class.java) + return (this as MathParsingResult.Success).result + } + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt deleted file mode 100644 index b66c801e1ba..00000000000 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt +++ /dev/null @@ -1,945 +0,0 @@ -package org.oppia.android.util.math - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.Test -import org.junit.runner.RunWith -import org.oppia.android.app.model.MathExpression -import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat -import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode -import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult -import org.robolectric.annotation.LooperMode - -/** Tests for [MathExpressionParser]. */ -@RunWith(AndroidJUnit4::class) -@LooperMode(LooperMode.Mode.PAUSED) -class ExpressionToPolynomialTest { - // TODO: add high-level checks for the three types, but don't test in detail since there are - // separate suites. Also, document the separate suites' existence in this suites's KDoc. - - @Test - fun testPolynomials() { - // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). - - val poly1 = parseNumericExpressionSuccessfully("1").toPolynomial() - assertThat(poly1).evaluatesToPlainTextThat().isEqualTo("1") - assertThat(poly1).isConstantThat().isIntegerThat().isEqualTo(1) - - val poly13 = parseNumericExpressionSuccessfully("1-1").toPolynomial() - assertThat(poly13).evaluatesToPlainTextThat().isEqualTo("0") - assertThat(poly13).isConstantThat().isIntegerThat().isEqualTo(0) - - val poly2 = parseNumericExpressionSuccessfully("3 + 4 * 2 / (1 - 5) ^ 2").toPolynomial() - assertThat(poly2).evaluatesToPlainTextThat().isEqualTo("7/2") - assertThat(poly2).isConstantThat().isRationalThat().apply { - hasNegativePropertyThat().isFalse() - hasWholeNumberThat().isEqualTo(3) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(2) - } - - val poly3 = - parseAlgebraicExpressionSuccessfullyWithAllErrors("133+3.14*x/(11-15)^2").toPolynomial() - assertThat(poly3).evaluatesToPlainTextThat().isEqualTo("0.19625x + 133") - assertThat(poly3).hasTermCountThat().isEqualTo(2) - assertThat(poly3).term(0).hasCoefficientThat().isIrrationalThat().isWithin(1e-5).of(0.19625) - assertThat(poly3).term(0).hasVariableCountThat().isEqualTo(1) - assertThat(poly3).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly3).term(0).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly3).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(133) - assertThat(poly3).term(1).hasVariableCountThat().isEqualTo(0) - - val poly4 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2").toPolynomial() - assertThat(poly4).evaluatesToPlainTextThat().isEqualTo("x^2") - assertThat(poly4).hasTermCountThat().isEqualTo(1) - assertThat(poly4).term(0).hasVariableCountThat().isEqualTo(1) - assertThat(poly4).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly4).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly4).term(0).variable(0).hasPowerThat().isEqualTo(2) - - val poly5 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy+x").toPolynomial() - assertThat(poly5).evaluatesToPlainTextThat().isEqualTo("xy + x") - assertThat(poly5).hasTermCountThat().isEqualTo(2) - assertThat(poly5).term(0).hasVariableCountThat().isEqualTo(2) - assertThat(poly5).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly5).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly5).term(0).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly5).term(0).variable(1).hasNameThat().isEqualTo("y") - assertThat(poly5).term(0).variable(1).hasPowerThat().isEqualTo(1) - assertThat(poly5).term(1).hasVariableCountThat().isEqualTo(1) - assertThat(poly5).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly5).term(1).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly5).term(1).variable(0).hasPowerThat().isEqualTo(1) - - val poly6 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2x").toPolynomial() - assertThat(poly6).evaluatesToPlainTextThat().isEqualTo("2x") - assertThat(poly6).hasTermCountThat().isEqualTo(1) - assertThat(poly6).term(0).hasVariableCountThat().isEqualTo(1) - assertThat(poly6).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(2) - assertThat(poly6).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly6).term(0).variable(0).hasPowerThat().isEqualTo(1) - - val poly30 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x+2").toPolynomial() - assertThat(poly30).evaluatesToPlainTextThat().isEqualTo("x + 2") - assertThat(poly30).hasTermCountThat().isEqualTo(2) - assertThat(poly30).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly30).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(2) - hasVariableCountThat().isEqualTo(0) - } - - val poly29 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2-3*x-10").toPolynomial() - assertThat(poly29).evaluatesToPlainTextThat().isEqualTo("x^2 - 3x - 10") - assertThat(poly29).hasTermCountThat().isEqualTo(3) - assertThat(poly29).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly29).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-3) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly29).term(2).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-10) - hasVariableCountThat().isEqualTo(0) - } - - val poly31 = parseAlgebraicExpressionSuccessfullyWithAllErrors("4*(x+2)").toPolynomial() - assertThat(poly31).evaluatesToPlainTextThat().isEqualTo("4x + 8") - assertThat(poly31).hasTermCountThat().isEqualTo(2) - assertThat(poly31).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(4) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly31).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(8) - hasVariableCountThat().isEqualTo(0) - } - - val poly7 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2xy^2z^3").toPolynomial() - assertThat(poly7).evaluatesToPlainTextThat().isEqualTo("2xy^2z^3") - assertThat(poly7).hasTermCountThat().isEqualTo(1) - assertThat(poly7).term(0).hasVariableCountThat().isEqualTo(3) - assertThat(poly7).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(2) - assertThat(poly7).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly7).term(0).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly7).term(0).variable(1).hasNameThat().isEqualTo("y") - assertThat(poly7).term(0).variable(1).hasPowerThat().isEqualTo(2) - assertThat(poly7).term(0).variable(2).hasNameThat().isEqualTo("z") - assertThat(poly7).term(0).variable(2).hasPowerThat().isEqualTo(3) - - // Show that 7+xy+yz-3-xz-yz+3xy-4 combines into 4xy-xz (the eliminated terms should be gone). - val poly8 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy+yz-xz-yz+3xy").toPolynomial() - assertThat(poly8).evaluatesToPlainTextThat().isEqualTo("4xy - xz") - assertThat(poly8).hasTermCountThat().isEqualTo(2) - assertThat(poly8).term(0).hasVariableCountThat().isEqualTo(2) - assertThat(poly8).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(4) - assertThat(poly8).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly8).term(0).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly8).term(0).variable(1).hasNameThat().isEqualTo("y") - assertThat(poly8).term(0).variable(1).hasPowerThat().isEqualTo(1) - assertThat(poly8).term(1).hasVariableCountThat().isEqualTo(2) - assertThat(poly8).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(-1) - assertThat(poly8).term(1).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly8).term(1).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly8).term(1).variable(1).hasNameThat().isEqualTo("z") - assertThat(poly8).term(1).variable(1).hasPowerThat().isEqualTo(1) - - // x+2x should become 3x since like terms are combined. - val poly9 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x+2x").toPolynomial() - assertThat(poly9).evaluatesToPlainTextThat().isEqualTo("3x") - assertThat(poly9).hasTermCountThat().isEqualTo(1) - assertThat(poly9).term(0).hasVariableCountThat().isEqualTo(1) - assertThat(poly9).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(3) - assertThat(poly9).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly9).term(0).variable(0).hasPowerThat().isEqualTo(1) - - // xx^2 should become x^3 since like terms are combined. - val poly10 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xx^2").toPolynomial() - assertThat(poly10).evaluatesToPlainTextThat().isEqualTo("x^3") - assertThat(poly10).hasTermCountThat().isEqualTo(1) - assertThat(poly10).term(0).hasVariableCountThat().isEqualTo(1) - assertThat(poly10).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly10).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly10).term(0).variable(0).hasPowerThat().isEqualTo(3) - - // No terms in this polynomial should be combined. - val poly11 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2+x+1").toPolynomial() - assertThat(poly11).evaluatesToPlainTextThat().isEqualTo("x^2 + x + 1") - assertThat(poly11).hasTermCountThat().isEqualTo(3) - assertThat(poly11).term(0).hasVariableCountThat().isEqualTo(1) - assertThat(poly11).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly11).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly11).term(0).variable(0).hasPowerThat().isEqualTo(2) - assertThat(poly11).term(1).hasVariableCountThat().isEqualTo(1) - assertThat(poly11).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly11).term(1).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly11).term(1).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly11).term(2).hasVariableCountThat().isEqualTo(0) - assertThat(poly11).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) - - // No terms in this polynomial should be combined. - val poly12 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2 + x^2y").toPolynomial() - assertThat(poly12).evaluatesToPlainTextThat().isEqualTo("x^2y + x^2") - assertThat(poly12).hasTermCountThat().isEqualTo(2) - assertThat(poly12).term(0).hasVariableCountThat().isEqualTo(2) - assertThat(poly12).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly12).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly12).term(0).variable(0).hasPowerThat().isEqualTo(2) - assertThat(poly12).term(0).variable(1).hasNameThat().isEqualTo("y") - assertThat(poly12).term(0).variable(1).hasPowerThat().isEqualTo(1) - assertThat(poly12).term(1).hasVariableCountThat().isEqualTo(1) - assertThat(poly12).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly12).term(1).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly12).term(1).variable(0).hasPowerThat().isEqualTo(2) - - // Ordering tests. Verify that ordering matches - // https://en.wikipedia.org/wiki/Polynomial#Definition (where multiple variables are sorted - // lexicographically). - - // The order of the terms in this polynomial should be reversed. - val poly14 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x+x^2+x^3").toPolynomial() - assertThat(poly14).evaluatesToPlainTextThat().isEqualTo("x^3 + x^2 + x + 1") - assertThat(poly14).hasTermCountThat().isEqualTo(4) - assertThat(poly14).term(0).hasVariableCountThat().isEqualTo(1) - assertThat(poly14).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly14).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly14).term(0).variable(0).hasPowerThat().isEqualTo(3) - assertThat(poly14).term(1).hasVariableCountThat().isEqualTo(1) - assertThat(poly14).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly14).term(1).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly14).term(1).variable(0).hasPowerThat().isEqualTo(2) - assertThat(poly14).term(2).hasVariableCountThat().isEqualTo(1) - assertThat(poly14).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly14).term(2).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly14).term(2).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly14).term(3).hasVariableCountThat().isEqualTo(0) - assertThat(poly14).term(3).hasCoefficientThat().isIntegerThat().isEqualTo(1) - - // The order of the terms in this polynomial should be preserved. - val poly15 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^3+x^2+x+1").toPolynomial() - assertThat(poly15).evaluatesToPlainTextThat().isEqualTo("x^3 + x^2 + x + 1") - assertThat(poly15).hasTermCountThat().isEqualTo(4) - assertThat(poly15).term(0).hasVariableCountThat().isEqualTo(1) - assertThat(poly15).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly15).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly15).term(0).variable(0).hasPowerThat().isEqualTo(3) - assertThat(poly15).term(1).hasVariableCountThat().isEqualTo(1) - assertThat(poly15).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly15).term(1).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly15).term(1).variable(0).hasPowerThat().isEqualTo(2) - assertThat(poly15).term(2).hasVariableCountThat().isEqualTo(1) - assertThat(poly15).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly15).term(2).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly15).term(2).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly15).term(3).hasVariableCountThat().isEqualTo(0) - assertThat(poly15).term(3).hasCoefficientThat().isIntegerThat().isEqualTo(1) - - // The order of the terms in this polynomial should be reversed. - val poly16 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy+xz+yz").toPolynomial() - assertThat(poly16).evaluatesToPlainTextThat().isEqualTo("xy + xz + yz") - assertThat(poly16).hasTermCountThat().isEqualTo(3) - assertThat(poly16).term(0).hasVariableCountThat().isEqualTo(2) - assertThat(poly16).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly16).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly16).term(0).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly16).term(0).variable(1).hasNameThat().isEqualTo("y") - assertThat(poly16).term(0).variable(1).hasPowerThat().isEqualTo(1) - assertThat(poly16).term(1).hasVariableCountThat().isEqualTo(2) - assertThat(poly16).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly16).term(1).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly16).term(1).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly16).term(1).variable(1).hasNameThat().isEqualTo("z") - assertThat(poly16).term(1).variable(1).hasPowerThat().isEqualTo(1) - assertThat(poly16).term(2).hasVariableCountThat().isEqualTo(2) - assertThat(poly16).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly16).term(2).variable(0).hasNameThat().isEqualTo("y") - assertThat(poly16).term(2).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly16).term(2).variable(1).hasNameThat().isEqualTo("z") - assertThat(poly16).term(2).variable(1).hasPowerThat().isEqualTo(1) - - // The order of the terms in this polynomial should be preserved. - val poly17 = parseAlgebraicExpressionSuccessfullyWithAllErrors("yz+xz+xy").toPolynomial() - assertThat(poly17).evaluatesToPlainTextThat().isEqualTo("xy + xz + yz") - assertThat(poly17).hasTermCountThat().isEqualTo(3) - assertThat(poly17).term(0).hasVariableCountThat().isEqualTo(2) - assertThat(poly17).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly17).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly17).term(0).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly17).term(0).variable(1).hasNameThat().isEqualTo("y") - assertThat(poly17).term(0).variable(1).hasPowerThat().isEqualTo(1) - assertThat(poly17).term(1).hasVariableCountThat().isEqualTo(2) - assertThat(poly17).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly17).term(1).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly17).term(1).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly17).term(1).variable(1).hasNameThat().isEqualTo("z") - assertThat(poly17).term(1).variable(1).hasPowerThat().isEqualTo(1) - assertThat(poly17).term(2).hasVariableCountThat().isEqualTo(2) - assertThat(poly17).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly17).term(2).variable(0).hasNameThat().isEqualTo("y") - assertThat(poly17).term(2).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly17).term(2).variable(1).hasNameThat().isEqualTo("z") - assertThat(poly17).term(2).variable(1).hasPowerThat().isEqualTo(1) - - val poly18 = - parseAlgebraicExpressionSuccessfullyWithAllErrors("3+x+y+xy+x^2y+xy^2+x^2y^2").toPolynomial() - assertThat(poly18).evaluatesToPlainTextThat().isEqualTo("x^2y^2 + x^2y + xy^2 + xy + x + y + 3") - assertThat(poly18).hasTermCountThat().isEqualTo(7) - assertThat(poly18).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly18).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly18).term(2).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly18).term(3).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly18).term(4).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly18).term(5).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly18).term(6).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(3) - hasVariableCountThat().isEqualTo(0) - } - - // Ensure variables of coefficient and power of 0 are removed. - val poly22 = parseAlgebraicExpressionSuccessfullyWithAllErrors("0x").toPolynomial() - assertThat(poly22).evaluatesToPlainTextThat().isEqualTo("0") - assertThat(poly22).hasTermCountThat().isEqualTo(1) - assertThat(poly22).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(0) - hasVariableCountThat().isEqualTo(0) - } - - val poly23 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x-x").toPolynomial() - assertThat(poly23).evaluatesToPlainTextThat().isEqualTo("0") - assertThat(poly23).hasTermCountThat().isEqualTo(1) - assertThat(poly23).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(0) - hasVariableCountThat().isEqualTo(0) - } - - val poly24 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^0").toPolynomial() - assertThat(poly24).evaluatesToPlainTextThat().isEqualTo("1") - assertThat(poly24).hasTermCountThat().isEqualTo(1) - assertThat(poly24).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(0) - } - - val poly25 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x/x").toPolynomial() - assertThat(poly25).evaluatesToPlainTextThat().isEqualTo("1") - assertThat(poly25).hasTermCountThat().isEqualTo(1) - assertThat(poly25).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(0) - } - - val poly26 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^(2-2)").toPolynomial() - assertThat(poly26).evaluatesToPlainTextThat().isEqualTo("1") - assertThat(poly26).hasTermCountThat().isEqualTo(1) - assertThat(poly26).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(0) - } - - val poly28 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x+1)/2").toPolynomial() - assertThat(poly28).evaluatesToPlainTextThat().isEqualTo("(1/2)x + 1/2") - assertThat(poly28).hasTermCountThat().isEqualTo(2) - assertThat(poly28).term(0).apply { - hasCoefficientThat().isRationalThat().apply { - hasNegativePropertyThat().isFalse() - hasWholeNumberThat().isEqualTo(0) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(2) - } - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly28).term(1).apply { - hasCoefficientThat().isRationalThat().apply { - hasNegativePropertyThat().isFalse() - hasWholeNumberThat().isEqualTo(0) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(2) - } - hasVariableCountThat().isEqualTo(0) - } - - // Ensure like terms are combined after polynomial multiplication. - val poly20 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x-5)(x+2)").toPolynomial() - assertThat(poly20).evaluatesToPlainTextThat().isEqualTo("x^2 - 3x - 10") - assertThat(poly20).hasTermCountThat().isEqualTo(3) - assertThat(poly20).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly20).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-3) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly20).term(2).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-10) - hasVariableCountThat().isEqualTo(0) - } - - val poly21 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(1+x)^3").toPolynomial() - assertThat(poly21).evaluatesToPlainTextThat().isEqualTo("x^3 + 3x^2 + 3x + 1") - assertThat(poly21).hasTermCountThat().isEqualTo(4) - assertThat(poly21).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(3) - } - } - assertThat(poly21).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(3) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly21).term(2).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(3) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly21).term(3).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(0) - } - - val poly27 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2*y^2 + 2").toPolynomial() - assertThat(poly27).evaluatesToPlainTextThat().isEqualTo("x^2y^2 + 2") - assertThat(poly27).hasTermCountThat().isEqualTo(2) - assertThat(poly27).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly27).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(2) - hasVariableCountThat().isEqualTo(0) - } - - val poly32 = - parseAlgebraicExpressionSuccessfullyWithAllErrors("(x^2-3*x-10)*(x+2)").toPolynomial() - assertThat(poly32).evaluatesToPlainTextThat().isEqualTo("x^3 - x^2 - 16x - 20") - assertThat(poly32).hasTermCountThat().isEqualTo(4) - assertThat(poly32).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(3) - } - } - assertThat(poly32).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly32).term(2).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-16) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly32).term(3).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-20) - hasVariableCountThat().isEqualTo(0) - } - - val poly33 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x-y)^3").toPolynomial() - assertThat(poly33).evaluatesToPlainTextThat().isEqualTo("x^3 - 3x^2y + 3xy^2 - y^3") - assertThat(poly33).hasTermCountThat().isEqualTo(4) - assertThat(poly33).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(3) - } - } - assertThat(poly33).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-3) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly33).term(2).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(3) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly33).term(3).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(3) - } - } - - // Ensure polynomial division works. - val poly19 = - parseAlgebraicExpressionSuccessfullyWithAllErrors("(x^2-3*x-10)/(x+2)").toPolynomial() - assertThat(poly19).evaluatesToPlainTextThat().isEqualTo("x - 5") - assertThat(poly19).hasTermCountThat().isEqualTo(2) - assertThat(poly19).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly19).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-5) - hasVariableCountThat().isEqualTo(0) - } - - val poly35 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(xy-5y)/y").toPolynomial() - assertThat(poly35).evaluatesToPlainTextThat().isEqualTo("x - 5") - assertThat(poly35).hasTermCountThat().isEqualTo(2) - assertThat(poly35).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly35).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-5) - hasVariableCountThat().isEqualTo(0) - } - - val poly36 = - parseAlgebraicExpressionSuccessfullyWithAllErrors("(x^2-2xy+y^2)/(x-y)").toPolynomial() - assertThat(poly36).evaluatesToPlainTextThat().isEqualTo("x - y") - assertThat(poly36).hasTermCountThat().isEqualTo(2) - assertThat(poly36).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly36).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(1) - } - } - - // Example from https://www.kristakingmath.com/blog/predator-prey-systems-ghtcp-5e2r4-427ab. - val poly37 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x^3-y^3)/(x-y)").toPolynomial() - assertThat(poly37).evaluatesToPlainTextThat().isEqualTo("x^2 + xy + y^2") - assertThat(poly37).hasTermCountThat().isEqualTo(3) - assertThat(poly37).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly37).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly37).term(2).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(2) - } - } - - // Multi-variable & more complex division. - val poly34 = - parseAlgebraicExpressionSuccessfullyWithAllErrors( - "(x^3-3x^2y+3xy^2-y^3)/(x-y)^2" - ).toPolynomial() - assertThat(poly34).evaluatesToPlainTextThat().isEqualTo("x - y") - assertThat(poly34).hasTermCountThat().isEqualTo(2) - assertThat(poly34).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly34).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(1) - } - } - - val poly38 = parseNumericExpressionSuccessfully("2^-4").toPolynomial() - assertThat(poly38).evaluatesToPlainTextThat().isEqualTo("1/16") - assertThat(poly38).hasTermCountThat().isEqualTo(1) - assertThat(poly38).term(0).apply { - hasCoefficientThat().isRationalThat().apply { - hasNegativePropertyThat().isFalse() - hasWholeNumberThat().isEqualTo(0) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(16) - } - hasVariableCountThat().isEqualTo(0) - } - - val poly39 = parseNumericExpressionSuccessfully("2^(3-6)").toPolynomial() - assertThat(poly39).evaluatesToPlainTextThat().isEqualTo("1/8") - assertThat(poly39).hasTermCountThat().isEqualTo(1) - assertThat(poly39).term(0).apply { - hasCoefficientThat().isRationalThat().apply { - hasNegativePropertyThat().isFalse() - hasWholeNumberThat().isEqualTo(0) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(8) - } - hasVariableCountThat().isEqualTo(0) - } - - // x^-3 is not a valid polynomial (since polynomials can't have negative powers). - val poly40 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^(3-6)").toPolynomial() - assertThat(poly40).isNotValidPolynomial() - - // 2^x is not a polynomial. - val poly41 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("2^x").toPolynomial() - assertThat(poly41).isNotValidPolynomial() - - // 1/x is not a polynomial. - val poly42 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("1/x").toPolynomial() - assertThat(poly42).isNotValidPolynomial() - - val poly43 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x/2").toPolynomial() - assertThat(poly43).evaluatesToPlainTextThat().isEqualTo("(1/2)x") - assertThat(poly43).hasTermCountThat().isEqualTo(1) - assertThat(poly43).term(0).apply { - hasCoefficientThat().isRationalThat().apply { - hasNegativePropertyThat().isFalse() - hasWholeNumberThat().isEqualTo(0) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(2) - } - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - - val poly44 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x-3)/2").toPolynomial() - assertThat(poly44).evaluatesToPlainTextThat().isEqualTo("(1/2)x - 3/2") - assertThat(poly44).hasTermCountThat().isEqualTo(2) - assertThat(poly44).term(0).apply { - hasCoefficientThat().isRationalThat().apply { - hasNegativePropertyThat().isFalse() - hasWholeNumberThat().isEqualTo(0) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(2) - } - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly44).term(1).apply { - hasCoefficientThat().isRationalThat().apply { - hasNegativePropertyThat().isTrue() - hasWholeNumberThat().isEqualTo(1) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(2) - } - hasVariableCountThat().isEqualTo(0) - } - - val poly45 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x-1)(x+1)").toPolynomial() - assertThat(poly45).evaluatesToPlainTextThat().isEqualTo("x^2 - 1") - assertThat(poly45).hasTermCountThat().isEqualTo(2) - assertThat(poly45).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly45).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-1) - hasVariableCountThat().isEqualTo(0) - } - - // √x is not a polynomial. - val poly46 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(x)").toPolynomial() - assertThat(poly46).isNotValidPolynomial() - - val poly47 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√(x^2)").toPolynomial() - assertThat(poly47).evaluatesToPlainTextThat().isEqualTo("x") - assertThat(poly47).hasTermCountThat().isEqualTo(1) - assertThat(poly47).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - - val poly51 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√(x^2y^2)").toPolynomial() - assertThat(poly51).evaluatesToPlainTextThat().isEqualTo("xy") - assertThat(poly51).hasTermCountThat().isEqualTo(1) - assertThat(poly51).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(1) - } - } - - // A limitation in the current polynomial conversion is that sqrt(x) will fail due to it not - // have any polynomial representation. - val poly48 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√x^2").toPolynomial() - assertThat(poly48).isNotValidPolynomial() - - // √(x^2+2) may evaluate to a polynomial, but it requires factoring (which isn't yet supported). - val poly50 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√(x^2+2)").toPolynomial() - assertThat(poly50).isNotValidPolynomial() - - // Division by zero is undefined, so a polynomial can't be constructed. - val poly49 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("(x+2)/0").toPolynomial() - assertThat(poly49).isNotValidPolynomial() - - val poly52 = parsePolynomialFromNumericExpression("1") - val poly53 = parsePolynomialFromNumericExpression("0") - assertThat(poly52).isNotEqualTo(poly53) - - val poly54 = parsePolynomialFromNumericExpression("1+2") - val poly55 = parsePolynomialFromNumericExpression("3") - assertThat(poly54).isEqualTo(poly55) - - val poly56 = parsePolynomialFromNumericExpression("1-2") - val poly57 = parsePolynomialFromNumericExpression("-1") - assertThat(poly56).isEqualTo(poly57) - - val poly58 = parsePolynomialFromNumericExpression("2*3") - val poly59 = parsePolynomialFromNumericExpression("6") - assertThat(poly58).isEqualTo(poly59) - - val poly60 = parsePolynomialFromNumericExpression("2^3") - val poly61 = parsePolynomialFromNumericExpression("8") - assertThat(poly60).isEqualTo(poly61) - - val poly62 = parsePolynomialFromAlgebraicExpression("1+x") - val poly63 = parsePolynomialFromAlgebraicExpression("x+1") - assertThat(poly62).isEqualTo(poly63) - - val poly64 = parsePolynomialFromAlgebraicExpression("y+x") - val poly65 = parsePolynomialFromAlgebraicExpression("x+y") - assertThat(poly64).isEqualTo(poly65) - - val poly66 = parsePolynomialFromAlgebraicExpression("(x+1)^2") - val poly67 = parsePolynomialFromAlgebraicExpression("x^2+2x+1") - assertThat(poly66).isEqualTo(poly67) - - val poly68 = parsePolynomialFromAlgebraicExpression("(x+1)/2") - val poly69 = parsePolynomialFromAlgebraicExpression("x/2+(1/2)") - assertThat(poly68).isEqualTo(poly69) - - val poly70 = parsePolynomialFromAlgebraicExpression("x*2") - val poly71 = parsePolynomialFromAlgebraicExpression("2x") - assertThat(poly70).isEqualTo(poly71) - - val poly72 = parsePolynomialFromAlgebraicExpression("x(x+1)") - val poly73 = parsePolynomialFromAlgebraicExpression("x^2+x") - assertThat(poly72).isEqualTo(poly73) - } - - private fun parsePolynomialFromNumericExpression(expression: String) = - parseNumericExpressionSuccessfully(expression).toPolynomial() - - private fun parsePolynomialFromAlgebraicExpression(expression: String) = - parseAlgebraicExpressionSuccessfullyWithAllErrors(expression).toPolynomial() - - private companion object { - // TODO: fix helper API. - - private fun parseNumericExpressionSuccessfully(expression: String): MathExpression { - val result = parseNumericExpressionWithAllErrors(expression) - return (result as MathParsingResult.Success).result - } - - private fun parseNumericExpressionWithAllErrors( - expression: String - ): MathParsingResult { - return MathExpressionParser.parseNumericExpression(expression, ErrorCheckingMode.ALL_ERRORS) - } - - private fun parseAlgebraicExpressionSuccessfullyWithAllErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathExpression { - val result = - parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) - return (result as MathParsingResult.Success).result - } - - private fun parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathExpression { - val result = - parseAlgebraicExpressionInternal( - expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables - ) - return (result as MathParsingResult.Success).result - } - - private fun parseAlgebraicExpressionInternal( - expression: String, - errorCheckingMode: ErrorCheckingMode, - allowedVariables: List = listOf("x", "y", "z") - ): MathParsingResult { - return MathExpressionParser.parseAlgebraicExpression( - expression, allowedVariables, errorCheckingMode - ) - } - } -} diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt index 85a734a7ac4..8be3ee05bc3 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt @@ -6,6 +6,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression +import org.oppia.android.testing.math.PolynomialSubject +import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult @@ -20,7 +22,8 @@ import org.robolectric.annotation.LooperMode * verifications for operations like LaTeX conversion and expression evaluation are part of more * targeted test suites such as [ExpressionToLatexConverterTest] and * [NumericExpressionEvaluatorTest]. For comparable operations, see - * [ExpressionToComparableOperationConverterTest]. + * [ExpressionToComparableOperationConverterTest]. For polynomials, see + * [ExpressionToPolynomialConverterTest]. */ // FunctionName: test names are conventionally named with underscores. // SameParameterValue: tests should have specific context included/excluded for readability. @@ -95,6 +98,43 @@ class MathExpressionExtensionsTest { assertThat(operation1).isNotEqualTo(operation2) } + @Test + fun testToPolynomial_algebraicExpression_returnsCorrectPolynomial() { + val expression = parseAlgebraicExpression("(x^3-y^3)/(x-y)") + + val polynomial = expression.toPolynomial() + + assertThat(polynomial).hasTermCountThat().isEqualTo(3) + assertThat(polynomial).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(polynomial).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(polynomial).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + } + private companion object { private fun parseNumericExpression(expression: String): MathExpression { return parseNumericExpression(expression, ALL_ERRORS).retrieveExpectedSuccessfulResult() diff --git a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt index 4fac2ae26b0..ed4887261ff 100644 --- a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt @@ -1,6 +1,5 @@ package org.oppia.android.util.math -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @@ -9,21 +8,24 @@ import org.oppia.android.app.model.Polynomial import org.oppia.android.app.model.Polynomial.Term import org.oppia.android.app.model.Polynomial.Term.Variable import org.oppia.android.app.model.Real +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.robolectric.annotation.LooperMode +import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat /** Tests for [Polynomial] extensions. */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") -@RunWith(AndroidJUnit4::class) +@RunWith(OppiaParameterizedTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class PolynomialExtensionsTest { private companion object { - private const val PI = 3.1415 - - private val ONE_HALF_FRACTION = Fraction.newBuilder().apply { + private val ONE_THIRD_FRACTION = Fraction.newBuilder().apply { numerator = 1 - denominator = 2 + denominator = 3 }.build() private val ONE_AND_ONE_HALF_FRACTION = Fraction.newBuilder().apply { @@ -32,40 +34,64 @@ class PolynomialExtensionsTest { wholeNumber = 1 }.build() - private val ZERO_REAL = Real.newBuilder().apply { - integer = 0 + private val THREE_ONES_FRACTION = Fraction.newBuilder().apply { + numerator = 3 + denominator = 1 }.build() - private val ONE_REAL = Real.newBuilder().apply { - integer = 1 + private val THREE_FRACTION = Fraction.newBuilder().apply { + wholeNumber = 3 + denominator = 1 }.build() private val TWO_REAL = Real.newBuilder().apply { integer = 2 }.build() - private val ONE_HALF_REAL = Real.newBuilder().apply { - rational = ONE_HALF_FRACTION + private val THREE_REAL = Real.newBuilder().apply { + integer = 3 + }.build() + + private val FOUR_REAL = Real.newBuilder().apply { + integer = 4 + }.build() + + private val FIVE_REAL = Real.newBuilder().apply { + integer = 5 + }.build() + + private val SEVEN_REAL = Real.newBuilder().apply { + integer = 7 + }.build() + + private val ONE_THIRD_REAL = Real.newBuilder().apply { + rational = ONE_THIRD_FRACTION }.build() private val ONE_AND_ONE_HALF_REAL = Real.newBuilder().apply { rational = ONE_AND_ONE_HALF_FRACTION }.build() - private val PI_REAL = Real.newBuilder().apply { - irrational = PI + private val THREE_ONES_REAL = Real.newBuilder().apply { + rational = THREE_ONES_FRACTION + }.build() + + private val THREE_FRACTION_REAL = Real.newBuilder().apply { + rational = THREE_FRACTION }.build() - private val ZERO_POLYNOMIAL = createPolynomial(createTerm(coefficient = ZERO_REAL)) + private val PI_REAL = Real.newBuilder().apply { + irrational = 3.14 + }.build() private val TWO_POLYNOMIAL = createPolynomial(createTerm(coefficient = TWO_REAL)) private val NEGATIVE_TWO_POLYNOMIAL = createPolynomial(createTerm(coefficient = -TWO_REAL)) - private val ONE_HALF_POLYNOMIAL = createPolynomial(createTerm(coefficient = ONE_HALF_REAL)) + private val ONE_HALF_POLYNOMIAL = createPolynomial(createTerm(coefficient = ONE_HALF)) private val NEGATIVE_ONE_HALF_POLYNOMIAL = - createPolynomial(createTerm(coefficient = -ONE_HALF_REAL)) + createPolynomial(createTerm(coefficient = -ONE_HALF)) private val ONE_AND_ONE_HALF_POLYNOMIAL = createPolynomial(createTerm(coefficient = ONE_AND_ONE_HALF_REAL)) @@ -78,21 +104,39 @@ class PolynomialExtensionsTest { private val NEGATIVE_PI_POLYNOMIAL = createPolynomial(createTerm(coefficient = -PI_REAL)) private val ONE_X_POLYNOMIAL = - createPolynomial(createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 1))) + createPolynomial(createTerm(coefficient = ONE, createVariable(name = "x", power = 1))) private val NEGATIVE_ONE_X_POLYNOMIAL = - createPolynomial(createTerm(coefficient = -ONE_REAL, createVariable(name = "x", power = 1))) + createPolynomial(createTerm(coefficient = -ONE, createVariable(name = "x", power = 1))) private val TWO_X_POLYNOMIAL = createPolynomial(createTerm(coefficient = TWO_REAL, createVariable(name = "x", power = 1))) private val ONE_PLUS_X_POLYNOMIAL = createPolynomial( - createTerm(coefficient = ONE_REAL), - createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 1)) + createTerm(coefficient = ONE), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) ) } + @Parameter lateinit var var1: String + @Parameter lateinit var var2: String + @Parameter lateinit var var3: String + + @Test + fun testZeroPolynomial_isEqualToZero() { + val subject = ZERO_POLYNOMIAL + + assertThat(subject).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testOnePolynomial_isEqualToOne() { + val subject = ONE_POLYNOMIAL + + assertThat(subject).isConstantThat().isIntegerThat().isEqualTo(1) + } + @Test fun testIsConstant_default_returnsFalse() { val defaultPolynomial = Polynomial.getDefaultInstance() @@ -175,7 +219,7 @@ class PolynomialExtensionsTest { @Test fun testIsConstant_one_and_two_returnsFalse() { val onePlusTwoPolynomial = - createPolynomial(createTerm(coefficient = ONE_REAL), createTerm(coefficient = TWO_REAL)) + createPolynomial(createTerm(coefficient = ONE), createTerm(coefficient = TWO_REAL)) val result = onePlusTwoPolynomial.isConstant() @@ -223,14 +267,14 @@ class PolynomialExtensionsTest { fun testGetConstant_pi_returnsPi() { val result = PI_POLYNOMIAL.getConstant() - assertThat(result).isIrrationalThat().isWithin(1e-5).of(PI) + assertThat(result).isIrrationalThat().isWithin(1e-5).of(3.14) } @Test fun testGetConstant_negativePi_returnsNegativePi() { val result = NEGATIVE_PI_POLYNOMIAL.getConstant() - assertThat(result).isIrrationalThat().isWithin(1e-5).of(-PI) + assertThat(result).isIrrationalThat().isWithin(1e-5).of(-3.14) } @Test @@ -272,14 +316,14 @@ class PolynomialExtensionsTest { fun testToPlainText_pi_returnsPiString() { val result = PI_POLYNOMIAL.toPlainText() - assertThat(result).isEqualTo("3.1415") + assertThat(result).isEqualTo("3.14") } @Test fun testToPlainText_negativePi_returnsMinusPiString() { val result = NEGATIVE_PI_POLYNOMIAL.toPlainText() - assertThat(result).isEqualTo("-3.1415") + assertThat(result).isEqualTo("-3.14") } @Test @@ -313,8 +357,8 @@ class PolynomialExtensionsTest { @Test fun testToPlainText_oneAndNegativeX_returnsOneMinusXString() { val oneMinusXPolynomial = createPolynomial( - createTerm(coefficient = ONE_REAL), - createTerm(coefficient = -ONE_REAL, createVariable(name = "x", power = 1)) + createTerm(coefficient = ONE), + createTerm(coefficient = -ONE, createVariable(name = "x", power = 1)) ) val result = oneMinusXPolynomial.toPlainText() @@ -326,7 +370,7 @@ class PolynomialExtensionsTest { fun testToPlainText_oneAndOneHalfXAndY_returnsThreeHalvesXPlusYString() { val oneMinusXPolynomial = createPolynomial( createTerm(coefficient = ONE_AND_ONE_HALF_REAL, createVariable(name = "x", power = 1)), - createTerm(coefficient = ONE_REAL, createVariable(name = "y", power = 1)) + createTerm(coefficient = ONE, createVariable(name = "y", power = 1)) ) val result = oneMinusXPolynomial.toPlainText() @@ -337,9 +381,9 @@ class PolynomialExtensionsTest { @Test fun testToPlainText_oneAndXAndXSquared_returnsOnePlusXPlusXSquaredString() { val oneMinusXPolynomial = createPolynomial( - createTerm(coefficient = ONE_REAL), - createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 1)), - createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 2)) + createTerm(coefficient = ONE), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) ) val result = oneMinusXPolynomial.toPlainText() @@ -350,9 +394,9 @@ class PolynomialExtensionsTest { @Test fun testToPlainText_xSquaredAndXAndOne_returnsXSquaredPlusXPlusOneString() { val oneMinusXPolynomial = createPolynomial( - createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 2)), - createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 1)), - createTerm(coefficient = ONE_REAL) + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE) ) val result = oneMinusXPolynomial.toPlainText() @@ -364,15 +408,2548 @@ class PolynomialExtensionsTest { @Test fun testToPlainText_xSquaredYCubedAndOne_returnsXSquaredYCubedPlusOneString() { val oneMinusXPolynomial = createPolynomial( - createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 2)), - createTerm(coefficient = ONE_REAL, createVariable(name = "y", power = 3)), - createTerm(coefficient = ONE_REAL) + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = ONE, createVariable(name = "y", power = 3)), + createTerm(coefficient = ONE) ) val result = oneMinusXPolynomial.toPlainText() assertThat(result).isEqualTo("x^2 + y^3 + 1") } + + @Test + fun testRemoveUnnecessaryVariables_zeroX_returnsZero() { + val polynomial = createPolynomial( + createTerm(coefficient = ZERO, createVariable(name = "x", power = 1)) + ) + + val result = polynomial.removeUnnecessaryVariables() + + // 0x becomes just 0. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testRemoveUnnecessaryVariables_xPlusZero_returnsX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ZERO) + ) + + val result = polynomial.removeUnnecessaryVariables() + + // x+0 is just x. + assertThat(result).isEqualTo(ONE_X_POLYNOMIAL) + } + + @Test + fun testRemoveUnnecessaryVariables_zeroXPlusOne_returnsOne() { + val polynomial = createPolynomial( + createTerm(coefficient = ZERO, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE) + ) + + val result = polynomial.removeUnnecessaryVariables() + + // 0x+1 is just 1. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(1) + } + + @Test + fun testRemoveUnnecessaryVariables_zeroXPlusZero_returnsZero() { + val polynomial = createPolynomial( + createTerm(coefficient = ZERO, createVariable(name = "x", power = 1)), + createTerm(coefficient = ZERO) + ) + + val result = polynomial.removeUnnecessaryVariables() + + // 0x+0 is just 0. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testRemoveUnnecessaryVariables_zeroXSquaredPlusZeroXPlusTwo_returnsTwo() { + val polynomial = createPolynomial( + createTerm(coefficient = ZERO, createVariable(name = "x", power = 2)), + createTerm(coefficient = ZERO, createVariable(name = "x", power = 1)), + createTerm(coefficient = TWO_REAL) + ) + + val result = polynomial.removeUnnecessaryVariables() + + // 0x^2+0x+2 is just 2. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(2) + } + + @Test + fun testRemoveUnnecessaryVariables_zeroPlusOnePlusZeroXPlusZero_returnsOne() { + val polynomial = createPolynomial( + createTerm(coefficient = ZERO), + createTerm(coefficient = ONE), + createTerm(coefficient = ZERO, createVariable(name = "x", power = 1)), + createTerm(coefficient = ZERO) + ) + + val result = polynomial.removeUnnecessaryVariables() + + // 0+1+0x+0 is just 1. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(1) + } + + @Test + fun testSimplifyRationals_oneX_returnsOneX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result = polynomial.simplifyRationals() + + // x stays as x. + assertThat(result).isEqualTo(ONE_X_POLYNOMIAL) + } + + @Test + fun testSimplifyRationals_oneHalfX_returnsOneHalfX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE_HALF, createVariable(name = "x", power = 1)) + ) + + val result = polynomial.simplifyRationals() + + // (1/2)x stays as (1/2)x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasVariableCountThat().isEqualTo(1) + hasCoefficientThat().isEqualTo(ONE_HALF) + variable(0).hasNameThat().isEqualTo("x") + variable(0).hasPowerThat().isEqualTo(1) + } + } + } + + @Test + fun testSimplifyRationals_threeOnesX_returnsThreeOnesX() { + val polynomial = createPolynomial( + createTerm(coefficient = THREE_ONES_REAL, createVariable(name = "x", power = 1)) + ) + + val result = polynomial.simplifyRationals() + + // (3/1)x stays as 3x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasVariableCountThat().isEqualTo(1) + hasCoefficientThat().isIntegerThat().isEqualTo(3) + variable(0).hasNameThat().isEqualTo("x") + variable(0).hasPowerThat().isEqualTo(1) + } + } + } + + @Test + fun testSimplifyRationals_negativeThreeXAsFraction_returnsNegativeThreeXWithInteger() { + val polynomial = createPolynomial( + createTerm(coefficient = -THREE_FRACTION_REAL, createVariable(name = "x", power = 1)) + ) + + val result = polynomial.simplifyRationals() + + // -3x (fraction) becomes -3x (integer). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasVariableCountThat().isEqualTo(1) + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + variable(0).hasNameThat().isEqualTo("x") + variable(0).hasPowerThat().isEqualTo(1) + } + } + } + + @Test + fun testSimplifyRationals_xPlusThreeFractionXSquared_returnsXPlusThreeXSquared() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = THREE_FRACTION_REAL, createVariable(name = "x", power = 2)) + ) + + val result = polynomial.simplifyRationals() + + // x+3x (fraction) becomes x+3x (integer). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasVariableCountThat().isEqualTo(1) + hasCoefficientThat().isIntegerThat().isEqualTo(1) + variable(0).hasNameThat().isEqualTo("x") + variable(0).hasPowerThat().isEqualTo(1) + } + term(1).apply { + hasVariableCountThat().isEqualTo(1) + hasCoefficientThat().isIntegerThat().isEqualTo(3) + variable(0).hasNameThat().isEqualTo("x") + variable(0).hasPowerThat().isEqualTo(2) + } + } + } + + @Test + fun testSort_one_returnsOne() { + val polynomial = createPolynomial(createTerm(coefficient = ONE)) + + val result = polynomial.sort() + + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(1) + } + + @Test + fun testSort_x_returnsX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result = polynomial.sort() + + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testSort_onePlusTwo_returnsTwoPlusOne() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE), createTerm(coefficient = TWO_REAL) + ) + + val result = polynomial.sort() + + // 1+2 becomes 2+1 (larger number sorted first). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(0) + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testSort_twoPlusOne_returnsTwoPlusOne() { + val polynomial = createPolynomial( + createTerm(coefficient = TWO_REAL), createTerm(coefficient = ONE) + ) + + val result = polynomial.sort() + + // 2+1 stays as 2+1 (larger number sorted first). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(0) + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testSort_xPlusX_returnsXPlusX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result = polynomial.sort() + + // x+x is symmetrical, so nothing changes. + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testSort_xPlusOne_returnsXPlusOne() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE) + ) + + val result = polynomial.sort() + + // x+1 stays as x+1 (variables are sorted first). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testSort_onePlusX_returnsXPlusOne() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result = polynomial.sort() + + // 1+x becomes x+1 (variables are sorted first). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testSort_xPlusTwoX_returnsTwoXPlusX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = TWO_REAL, createVariable(name = "x", power = 1)) + ) + + val result = polynomial.sort() + + // x+2x becomes 2x+x (larger coefficients are sorted first). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testSort_xPlusXSquared_returnsXSquaredPlusX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + + val result = polynomial.sort() + + // x+x^2 becomes x^2+x (larger powers are sorted first). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testSort_xSquaredPlusX_returnsXSquaredPlusX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result = polynomial.sort() + + // x^2+x stays as x^2+x (larger powers are sorted first). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testSort_xMinusXSquared_returnsNegativeXSquaredPlusX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = -ONE, createVariable(name = "x", power = 2)) + ) + + val result = polynomial.sort() + + // 1-x^2 becomes -x^2+1 (larger powers are sorted first). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testSort_negativeXSquaredPlusX_returnsNegativeXSquaredPlusX() { + val polynomial = createPolynomial( + createTerm(coefficient = -ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result = polynomial.sort() + + // -x^2+1 stays as -x^2+1 (larger powers are sorted first). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testSort_yPlusXy_returnsXyPlusY() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "y", power = 1)), + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1) + ) + ) + + val result = polynomial.sort() + + // y+xy becomes xy+y (x variables are sorted first). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testSort_xPlusXy_returnsXyPlusX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1) + ) + ) + + val result = polynomial.sort() + + // x+xy becomes xy+x (more variables are sorted first) + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testSort_xyPlusZyx_returnsXyzPlusXy() { + val polynomial = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1) + ), + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1), + createVariable(name = "z", power = 1) + ) + ) + + val result = polynomial.sort() + + // xy+zyx becomes xyz+xy (again, more variables are sorted first). Also, variables are + // rearranged lexicographically. + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(3) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + variable(2).apply { + hasNameThat().isEqualTo("z") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testSort_zyPlusYx_returnsXyPlusYz() { + val polynomial = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "z", power = 1), + createVariable(name = "y", power = 1) + ), + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1) + ) + ) + + val result = polynomial.sort() + + // zy+yx becomes xy+yz (sorted lexicographically). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("z") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testSort_xyzPlusYXSquared_returnsXSquaredYPlusXyz() { + val polynomial = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1), + createVariable(name = "z", power = 1) + ), + createTerm( + coefficient = ONE, + createVariable(name = "y", power = 1), + createVariable(name = "x", power = 2) + ) + ) + + val result = polynomial.sort() + + // xyz+yx^2 becomes x^2y+xyz (despite xyz having more variables, the higher power of x^2y + // prioritizes it). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(3) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + variable(2).apply { + hasNameThat().isEqualTo("z") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testSort_xSquaredY_plusX_plusYCubed_plusXSquared_returnsYCubedPlusXSqYPlusXSqPlusX() { + val polynomial = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "y", power = 3)), + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + + val result = polynomial.sort() + + // x^2y+x+y^3+x^2 becomes x^2y+x^2+x+y^3 per rules demonstrated in earlier tests. This test + // brings more of them together in one example, plus note that x terms are always fully listed + // first. + assertThat(result).apply { + hasTermCountThat().isEqualTo(4) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(3) + } + } + } + } + + @Test + @RunParameterized( + Iteration("x+y+z", "var1=x", "var2=y", "var3=z"), + Iteration("x+z+y", "var1=x", "var2=z", "var3=y"), + Iteration("y+x+z", "var1=y", "var2=x", "var3=z"), + Iteration("y+z+x", "var1=y", "var2=z", "var3=x"), + Iteration("z+x+y", "var1=z", "var2=x", "var3=y"), + Iteration("z+y+x", "var1=z", "var2=y", "var3=x") + ) + fun testSort_xPlusYPlusZ_inAnyOrder_returnsXPlusYPlusZ() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = var1, power = 1)), + createTerm(coefficient = ONE, createVariable(name = var2, power = 1)), + createTerm(coefficient = ONE, createVariable(name = var3, power = 1)) + ) + + val result = polynomial.sort() + + // Regardless of what order x, y, and z are combined in a polynomial, the sorted result is + // always x+y+z (per lexicographical sorting of the variable names themselves). + assertThat(result).apply { + hasTermCountThat().isEqualTo(3) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("z") + hasPowerThat().isEqualTo(1) + } + } + } + } + + /* Operator tests. */ + + @Test + fun testUnaryMinus_zero_returnsZero() { + val polynomial = ZERO_POLYNOMIAL + + val result = -polynomial + + // negate(0) stays as 0. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testUnaryMinus_one_returnsNegativeOne() { + val polynomial = ONE_POLYNOMIAL + + val result = -polynomial + + // negate(1) becomes -1. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(-1) + } + + @Test + fun testUnaryMinus_x_returnsNegativeX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result = -polynomial + + // negate(x) becomes -x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testUnaryMinus_negativeX_returnsX() { + val polynomial = createPolynomial( + createTerm(coefficient = -ONE, createVariable(name = "x", power = 1)) + ) + + val result = -polynomial + + // negate(-x) becomes x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testUnaryMinus_xSquaredPlusX_returnsNegativeXSquaredMinusX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result = -polynomial + + // negate(x^2+x) becomes -x^2-x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testUnaryMinus_oneMinusX_returnsNegativeOnePlusX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE), + createTerm(coefficient = -ONE, createVariable(name = "x", power = 1)) + ) + + val result = -polynomial + + // negate(1-x) becomes -1+x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(0) + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testPlus_zeroAndZero_returnsZero() { + val polynomial1 = ZERO_POLYNOMIAL + val polynomial2 = ZERO_POLYNOMIAL + + val result = polynomial1 + polynomial2 + + // 0+0=0. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testPlus_zeroAndOne_returnsOne() { + val polynomial1 = ZERO_POLYNOMIAL + val polynomial2 = ONE_POLYNOMIAL + + val result = polynomial1 + polynomial2 + + // 0+1=1. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(1) + } + + @Test + fun testPlus_zeroAndX_returnsX() { + val polynomial1 = ZERO_POLYNOMIAL + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 + polynomial2 + + // 0+x=x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testPlus_oneAndX_returnsOnePlusX() { + val polynomial1 = ONE_POLYNOMIAL + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 + polynomial2 + + // poly(1)+poly(x)=poly(1+x). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testPlus_xAndOne_returnsXPlusOne() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = ONE_POLYNOMIAL + + val result = polynomial1 + polynomial2 + + // poly(x)+poly(1)=poly(x+1). Per sorting, this shows commutativity with the above operation. + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testPlus_xAndX_returnsTwoX() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 + polynomial2 + + // x+x=2x (shows combining like terms). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testPlus_xAndNegativeX_returnsZero() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = NEGATIVE_ONE_X_POLYNOMIAL + + val result = polynomial1 + polynomial2 + + // x+-x=0 (term elimination). + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testPlus_xSquaredAndX_returnsXSquaredPlusX() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 + polynomial2 + + // poly(x^2)+poly(x)=poly(x^+x). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testMinus_zeroAndZero_returnsZero() { + val polynomial1 = ZERO_POLYNOMIAL + val polynomial2 = ZERO_POLYNOMIAL + + val result = polynomial1 - polynomial2 + + // 0-0=0 (term elimination). + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testMinus_oneAndZero_returnsOne() { + val polynomial1 = ONE_POLYNOMIAL + val polynomial2 = ZERO_POLYNOMIAL + + val result = polynomial1 - polynomial2 + + // 1-0=1. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(1) + } + + @Test + fun testMinus_xAndZero_returnsX() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = ZERO_POLYNOMIAL + + val result = polynomial1 - polynomial2 + + // x-0=x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testMinus_xAndOne_returnsXMinusOne() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = ONE_POLYNOMIAL + + val result = polynomial1 - polynomial2 + + // poly(x)-poly(1)=poly(x-1). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testMinus_oneAndX_returnsOneMinusX() { + val polynomial1 = ONE_POLYNOMIAL + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 - polynomial2 + + // poly(1)-poly(x)=poly(1-x). Shows anticommutativity. + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testMinus_twoXAndX_returnsX() { + val polynomial1 = TWO_X_POLYNOMIAL + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 - polynomial2 + + // 2x-x=x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testMinus_xAndX_returnsZero() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 - polynomial2 + + // x-x=0. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testMinus_xAndNegativeX_returnsTwoX() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = NEGATIVE_ONE_X_POLYNOMIAL + + val result = polynomial1 - polynomial2 + + // x - -x=2x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testMinus_negativeXAndX_returnsNegativeTwoX() { + val polynomial1 = NEGATIVE_ONE_X_POLYNOMIAL + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 - polynomial2 + + // -x - x=-2x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-2) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testMinus_negativeXAndNegativeX_returnsZero() { + val polynomial1 = NEGATIVE_ONE_X_POLYNOMIAL + val polynomial2 = NEGATIVE_ONE_X_POLYNOMIAL + + val result = polynomial1 - polynomial2 + + // -x - -x=0. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testMinus_xSquaredAndX_returnsXSquaredMinusX() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 - polynomial2 + + // poly(x^2)-poly(x)=poly(x^2-x). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testTimes_zeroAndZero_returnsZero() { + val polynomial1 = ZERO_POLYNOMIAL + val polynomial2 = ZERO_POLYNOMIAL + + val result = polynomial1 * polynomial2 + + // 0*0=0. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testTimes_zeroAndOne_returnsZero() { + val polynomial1 = ZERO_POLYNOMIAL + val polynomial2 = ONE_POLYNOMIAL + + val result = polynomial1 * polynomial2 + + // 0*1=0. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testTimes_oneAndOne_returnsOne() { + val polynomial1 = ONE_POLYNOMIAL + val polynomial2 = ONE_POLYNOMIAL + + val result = polynomial1 * polynomial2 + + // 1*1=1. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(1) + } + + @Test + fun testTimes_twoAndThree_returnsSix() { + val polynomial1 = TWO_POLYNOMIAL + val polynomial2 = createPolynomial(createTerm(coefficient = THREE_REAL)) + + val result = polynomial1 * polynomial2 + + // 2*3=6. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(6) + } + + @Test + fun testTimes_xAndZero_returnsZero() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = ZERO_POLYNOMIAL + + val result = polynomial1 * polynomial2 + + // x*0=0. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testTimes_xAndOne_returnsX() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = ONE_POLYNOMIAL + + val result = polynomial1 * polynomial2 + + // x*1=x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testTimes_xAndX_returnsXSquared() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 * polynomial2 + + // x*x=x^2. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + } + + @Test + fun testTimes_threeXSquaredAndTwoX_returnsSixXCubed() { + val polynomial1 = createPolynomial( + createTerm(coefficient = THREE_REAL, createVariable(name = "x", power = 2)) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = TWO_REAL, createVariable(name = "x", power = 1)) + ) + + val result = polynomial1 * polynomial2 + + // 3x^2*2x=6x^3. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(6) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + } + } + + @Test + fun testTimes_twoXAndThreeXSquared_returnsSixXCubed() { + val polynomial1 = createPolynomial( + createTerm(coefficient = TWO_REAL, createVariable(name = "x", power = 1)) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = THREE_REAL, createVariable(name = "x", power = 2)) + ) + + val result = polynomial1 * polynomial2 + + // 2x*3x^2=6x^3. This demonstrates multiplication commutativity. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(6) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + } + } + + @Test + fun testTimes_xAndNegativeX_returnsNegativeXSquared() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = NEGATIVE_ONE_X_POLYNOMIAL + + val result = polynomial1 * polynomial2 + + // x*(-x)=-x^2. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + } + + @Test + fun testTimes_negativeXAndX_returnsNegativeXSquared() { + val polynomial1 = NEGATIVE_ONE_X_POLYNOMIAL + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 * polynomial2 + + // (-x)*x=-x^2. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + } + + @Test + fun testTimes_negativeXAndNegativeX_returnsXSquared() { + val polynomial1 = NEGATIVE_ONE_X_POLYNOMIAL + val polynomial2 = NEGATIVE_ONE_X_POLYNOMIAL + + val result = polynomial1 * polynomial2 + + // (-x)*(-x)=x^2 (negatives cancel out). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + } + + @Test + fun testTimes_negativeFiveX_sevenX_returnsNegativeThirtyFiveXSquared() { + val polynomial1 = createPolynomial( + createTerm(coefficient = -FIVE_REAL, createVariable(name = "x", power = 1)) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = SEVEN_REAL, createVariable(name = "x", power = 1)) + ) + + val result = polynomial1 * polynomial2 + + // -5x*7x=-35x^2. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-35) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + } + + @Test + fun testTimes_onePlusX_onePlusX_returnsOnePlus2XPlusXSquared() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result = polynomial1 * polynomial2 + + // (1+x)*(1+x)=1+2x+x^2 (like terms are combined). + assertThat(result).apply { + hasTermCountThat().isEqualTo(3) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + } + + @Test + fun testTimes_xPlusOne_xMinusOne_returnsXSquaredMinusOne() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = -ONE) + ) + + val result = polynomial1 * polynomial2 + + // (x+1)*(x-1)=x^2-1 (negative terms are eliminated). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testTimes_xMinusOne_xPlusOne_returnsXSquaredMinusOne() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = -ONE) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE) + ) + + val result = polynomial1 * polynomial2 + + // (x-1)*(x+1)=x^2-1 (commutativity works for combining terms, too). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testTimes_twoXy_threeXSquaredY_returnsSixXCubedYSquared() { + val polynomial1 = createPolynomial( + createTerm( + coefficient = TWO_REAL, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1) + ) + ) + val polynomial2 = createPolynomial( + createTerm( + coefficient = THREE_REAL, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + + val result = polynomial1 * polynomial2 + + // 2xy*3x^2y=6x^3y^2. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(6) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + } + } + + @Test + fun testDiv_oneAndZero_returnsNull() { + val polynomial1 = ONE_POLYNOMIAL + val polynomial2 = ZERO_POLYNOMIAL + + val result = polynomial1 / polynomial2 + + // Cannot divide by zero. + assertThat(result).isNotValidPolynomial() + } + + @Test + fun testDiv_threeAndTwo_returnsOneAndOneHalf() { + val polynomial1 = createPolynomial(createTerm(coefficient = THREE_REAL)) + val polynomial2 = TWO_POLYNOMIAL + + val result = polynomial1 / polynomial2 + + // 3/2=1 1/2 (fraction) to demonstrate fully constant division. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(1) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testDiv_piAndTwo_returnsHalfPi() { + val polynomial1 = PI_POLYNOMIAL + val polynomial2 = TWO_POLYNOMIAL + + val result = polynomial1 / polynomial2 + + // 3.14/2=1.57 (irrational) to demonstrate fully constant division. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIrrationalThat().isWithin(1e-5).of(1.57) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testDiv_xAndOne_returnsX() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = ONE_POLYNOMIAL + + val result = polynomial1 / polynomial2 + + // x/1=x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testDiv_oneAndX_returnsNull() { + val polynomial1 = ONE_POLYNOMIAL + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 / polynomial2 + + // 1/x fails (cannot have negative power terms in polynomials, and this also shows that division + // is not commutative). + assertThat(result).isNotValidPolynomial() + } + + @Test + fun testDiv_xSquared_x_returnsX() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 / polynomial2 + + // x^2/x=x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testDiv_onePlus2XPlusXSquared_onePlusX_returnsOnePlusX() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE), + createTerm(coefficient = TWO_REAL, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result = polynomial1 / polynomial2 + + // (1+2x+x^2)/(1+x)=x+1 (full polynomial division). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testDiv_xSquaredPlus2XPlusOne_onePlusX_returnsOnePlusX() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = TWO_REAL, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result = polynomial1 / polynomial2 + + // (x^2+2x+1)/(1+x)=x+1 (order of terms for the dividend doesn't matter). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testDiv_xSquaredPlus2XPlusOne_oneMinusX_returnsNull() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = TWO_REAL, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE), + createTerm(coefficient = -ONE, createVariable(name = "x", power = 1)) + ) + + val result = polynomial1 / polynomial2 + + // (x^2+2x+1)/(1-x) fails (division doesn't result in a perfect polynomial). + assertThat(result).isNotValidPolynomial() + } + + @Test + fun testDiv_negativeXCubed_xSquared_returnsNegativeX() { + val polynomial1 = createPolynomial( + createTerm(coefficient = -ONE, createVariable(name = "x", power = 3)) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + + val result = polynomial1 / polynomial2 + + // -x^3/x^2=-x (negatives are retained). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testDiv_xSquaredMinusOne_xPlusOne_returnsXMinusOne() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = -ONE) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE) + ) + + val result = polynomial1 / polynomial2 + + // (x^2-1)/(x+1)=x-1. + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testDiv_xSquaredMinusOne_xMinusOne_returnsXPlusOne() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = -ONE) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = -ONE) + ) + + val result = polynomial1 / polynomial2 + + // (x^2-1)/(x-1)=x+1. + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testDiv_xSquaredMinusOne_x_returnsNull() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = -ONE) + ) + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 / polynomial2 + + // (x^2-1)/x fails. + assertThat(result).isNotValidPolynomial() + } + + @Test + fun testDiv_xSquaredMinusOne_negativeOne_negativeXSquaredPlusOne() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = -ONE) + ) + val polynomial2 = createPolynomial(createTerm(coefficient = -ONE)) + + val result = polynomial1 / polynomial2 + + // (x^2-1)/(-1)=-x^2+1 (reverses negative signs). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testDiv_xSquaredMinusOne_two_returnsNull() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = -ONE) + ) + val polynomial2 = TWO_POLYNOMIAL + + val result = polynomial1 / polynomial2 + + // (x^2-1)/2=(1/2)x^2-1/2 (since non-zero constants can always be factored). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isEqualTo(ONE_HALF) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isEqualTo(-ONE_HALF) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testDiv_negativeThreeXSquared_xSquared_returnsNegativeThree() { + val polynomial1 = createPolynomial( + createTerm(coefficient = -THREE_REAL, createVariable(name = "x", power = 2)) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + + val result = polynomial1 / polynomial2 + + // (-3x^2)/(x^2)=-3 (coefficient is retained). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testDiv_negativeThreeXSquared_negativeXSquared_returnsThree() { + val polynomial1 = createPolynomial( + createTerm(coefficient = -THREE_REAL, createVariable(name = "x", power = 2)) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = -ONE, createVariable(name = "x", power = 2)) + ) + + val result = polynomial1 / polynomial2 + + // (-3x^2)/(-x^2)=3 (negatives cancel during division). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testDiv_xSquaredY_y_returnsXSquared() { + val polynomial1 = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "y", power = 1)) + ) + + val result = polynomial1 / polynomial2 + + // x^2y / y=x^2 (variable elimination). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + } + + @Test + fun testDiv_xSquaredY_x_returnsXTimesY() { + val polynomial1 = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result = polynomial1 / polynomial2 + + // x^2y / x=xy (variable power elimination). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testDiv_xSquaredY_xSquared_returnsY() { + val polynomial1 = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + + val result = polynomial1 / polynomial2 + + // x^2y / x^2=y (variable elimination). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testDiv_xSquaredY_yXSquared_returnsOne() { + val polynomial1 = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + val polynomial2 = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "y", power = 1), + createVariable(name = "x", power = 2) + ) + ) + + val result = polynomial1 / polynomial2 + + // x^2y / yx^2=1 (multi-variable elimination). + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(1) + } + + @Test + fun testDiv_xSquaredY_ySquared_returnsNull() { + val polynomial1 = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "y", power = 2)) + ) + + val result = polynomial1 / polynomial2 + + // x^2y / y^2 fails (no polynomial exists). + assertThat(result).isNotValidPolynomial() + } + + @Test + fun testPow_zeroAndZero_returnsOne() { + val polynomial1 = ZERO_POLYNOMIAL + val polynomial2 = ZERO_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // 0^0=1 (conventionally despite this power not existing in mathematics). + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(1) + } + + @Test + fun testPow_zeroAndOne_returnsZero() { + val polynomial1 = ZERO_POLYNOMIAL + val polynomial2 = ONE_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // 0^1=0. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testPow_oneAndZero_returnsOne() { + val polynomial1 = ONE_POLYNOMIAL + val polynomial2 = ZERO_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // 1^0=1 (i.e. exponentiation is not commutative). + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(1) + } + + @Test + fun testPow_oneAndOne_returnsOne() { + val polynomial1 = ONE_POLYNOMIAL + val polynomial2 = ONE_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // 1^1=1. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(1) + } + + @Test + fun testPow_xAndOne_returnsX() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = ONE_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // poly(x)^poly(1)=poly(x). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testPow_xAndX_returnsNull() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // x^x fails since polynomials can't have variable exponents. + assertThat(result).isNotValidPolynomial() + } + + @Test + fun testPow_xAndTwo_returnsXSquared() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = TWO_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // poly(x)^poly(2)=poly(x^2). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + } + + @Test + fun testPow_onePlusX_two_onePlus2XPlusXSquared() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + val polynomial2 = TWO_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // (1+x)^2=1+2x+x^2 (binomial expansion). + assertThat(result).apply { + hasTermCountThat().isEqualTo(3) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + } + + @Test + fun testPow_x_negativeOne_returnsNull() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = createPolynomial(createTerm(coefficient = -ONE)) + + val result = polynomial1 pow polynomial2 + + // x^-1 fails since polynomials can't have negative powers. + assertThat(result).isNotValidPolynomial() + } + + @Test + fun testPow_two_negativeOne_returnsOneHalf() { + val polynomial1 = TWO_POLYNOMIAL + val polynomial2 = createPolynomial(createTerm(coefficient = -ONE)) + + val result = polynomial1 pow polynomial2 + + // 2^-1=1/2 (this demonstrates constant-only powers, and that negative powers sometimes work). + assertThat(result).isConstantThat().isEqualTo(ONE_HALF) + } + + @Test + fun testPow_four_negativeOneHalf_returnsOneHalf() { + val polynomial1 = createPolynomial(createTerm(coefficient = FOUR_REAL)) + val polynomial2 = NEGATIVE_ONE_HALF_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // 4^(-1/2)=1/2 (this demonstrates constant-only powers, and that negative powers sometimes work). + assertThat(result).isConstantThat().isEqualTo(ONE_HALF) + } + + @Test + fun testPow_onePlusX_oneHalf_returnsNull() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + val polynomial2 = ONE_HALF_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // (1+x)^(1/2) fails since 1+x has no square root. + assertThat(result).isNotValidPolynomial() + } + + @Test + fun testPow_onePlus2XPlusXSquared_oneHalf_returnsNull() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE), + createTerm(coefficient = TWO_REAL, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + val polynomial2 = ONE_HALF_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // (1+2x+x^2)^(1/2) fails since multi-term factoring is not currently supported. + assertThat(result).isNotValidPolynomial() + } + + @Test + fun testPow_xSquaredMinusOne_oneHalf_returnsNull() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = -ONE) + ) + val polynomial2 = ONE_HALF_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // (x^2-1)^(1/2) fails since multi-term factoring is not currently supported. + assertThat(result).isNotValidPolynomial() + } + + @Test + fun testPow_xSquared_oneHalf_returnsX() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + val polynomial2 = ONE_HALF_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // (x^2)^(1/2)=x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testPow_fourXSquared_oneHalf_returnsTwoX() { + val polynomial1 = createPolynomial( + createTerm(coefficient = FOUR_REAL, createVariable(name = "x", power = 2)) + ) + val polynomial2 = ONE_HALF_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // (4x^2)^(1/2)=2x (demonstrates that coefficients can also be rooted). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testPow_xSquared_negativeOneHalf_returnsNull() { + val polynomial1 = createPolynomial( + createTerm(coefficient = -ONE, createVariable(name = "x", power = 2)) + ) + val polynomial2 = ONE_HALF_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // (-x^2)^(1/2) fails since a negative coefficient can't be square rooted. + assertThat(result).isNotValidPolynomial() + } + + @Test + fun testPow_negativeTwentySevenXCubed_oneThird_returnsNegativeThreeX() { + val twentySevenReal = checkNotNull(THREE_REAL pow THREE_REAL) + val polynomial1 = createPolynomial( + createTerm(coefficient = -twentySevenReal, createVariable(name = "x", power = 3)) + ) + val polynomial2 = createPolynomial(createTerm(coefficient = ONE_THIRD_REAL)) + + val result = polynomial1 pow polynomial2 + + // (-9x^3)^(1/3)=-3x (demonstrates real number rooting, i.e. support for negative coefficients + // in certain cases). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testPow_xSquared_oneThird_returnsNull() { + val twentySevenReal = checkNotNull(THREE_REAL pow THREE_REAL) + val polynomial1 = createPolynomial( + createTerm(coefficient = twentySevenReal, createVariable(name = "x", power = 2)) + ) + val polynomial2 = createPolynomial(createTerm(coefficient = ONE_THIRD_REAL)) + + val result = polynomial1 pow polynomial2 + + // (27x^2)^(1/3) fails since the power '2' cannot be taken to the 1/3 (i.e. 2/3 is not a valid + // polynomial power). + assertThat(result).isNotValidPolynomial() + } + + @Test + fun testPow_xAndPi_returnsNull() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = PI_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // Cannot raise polynomials to non-integer powers. + assertThat(result).isNotValidPolynomial() + } } private fun createVariable(name: String, power: Int) = Variable.newBuilder().apply { diff --git a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt index 43234d63feb..978b4a014f9 100644 --- a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt @@ -10,6 +10,7 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.math.RealSubject import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.robolectric.annotation.LooperMode @@ -51,6 +52,16 @@ class RealExtensionsTest { wholeNumber = 1 }.build() + private val THREE_FRACTION = Fraction.newBuilder().apply { + wholeNumber = 3 + denominator = 1 + }.build() + + private val THREE_ONES_FRACTION = Fraction.newBuilder().apply { + numerator = 3 + denominator = 1 + }.build() + private val ZERO_REAL = createIntegerReal(0) private val TWO_REAL = createIntegerReal(2) private val NEGATIVE_TWO_REAL = createIntegerReal(-2) @@ -59,6 +70,9 @@ class RealExtensionsTest { private val NEGATIVE_ONE_HALF_REAL = createRationalReal(-ONE_HALF_FRACTION) private val ONE_AND_ONE_HALF_REAL = createRationalReal(ONE_AND_ONE_HALF_FRACTION) private val NEGATIVE_ONE_AND_ONE_HALF_REAL = createRationalReal(-ONE_AND_ONE_HALF_FRACTION) + private val THREE_FRACTION_REAL = createRationalReal(THREE_FRACTION) + private val NEGATIVE_THREE_FRACTION_REAL = createRationalReal(-THREE_FRACTION) + private val THREE_ONES_REAL = createRationalReal(THREE_ONES_FRACTION) private val PI_REAL = createIrrationalReal(PI) private val NEGATIVE_PI_REAL = createIrrationalReal(-PI) @@ -76,6 +90,32 @@ class RealExtensionsTest { @Parameter lateinit var expFrac: String @Parameter var expDouble: Double = Double.MIN_VALUE + @Test + fun testZero_isZeroInteger() { + val subject = ZERO + + assertThat(subject).isIntegerThat().isEqualTo(0) + } + + @Test + fun testOne_isOneInteger() { + val subject = ONE + + assertThat(subject).isIntegerThat().isEqualTo(1) + } + + @Test + fun testOneHalf_isOneHalfRational() { + val subject = ONE_HALF + + assertThat(subject).isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + } + @Test fun testIsRational_default_returnsFalse() { val defaultReal = Real.getDefaultInstance() @@ -136,6 +176,76 @@ class RealExtensionsTest { assertThat(result).isFalse() } + @Test + fun testIsWholeNumber_default_returnsFalse() { + val defaultReal = Real.getDefaultInstance() + + val result = defaultReal.isWholeNumber() + + assertThat(result).isFalse() + } + + @Test + fun testIsWholeNumber_twoInteger_returnsTrue() { + val result = TWO_REAL.isWholeNumber() + + assertThat(result).isTrue() + } + + @Test + fun testIsWholeNumber_negativeTwoInteger_returnsTrue() { + val result = NEGATIVE_TWO_REAL.isWholeNumber() + + assertThat(result).isTrue() + } + + @Test + fun testIsWholeNumber_oneHalfFraction_returnsFalse() { + val result = ONE_HALF_REAL.isWholeNumber() + + assertThat(result).isFalse() + } + + @Test + fun testIsWholeNumber_threeOnesFraction_returnsFalse() { + val result = THREE_ONES_REAL.isWholeNumber() + + // 3/1 is treated as a fraction despite being numerically equivalent to a whole number. + assertThat(result).isFalse() + } + + @Test + fun testIsWholeNumber_threeFraction_returnsTrue() { + val result = THREE_FRACTION_REAL.isWholeNumber() + + assertThat(result).isTrue() + } + + @Test + fun testIsWholeNumber_negativeThreeFraction_returnsTrue() { + val result = NEGATIVE_THREE_FRACTION_REAL.isWholeNumber() + + assertThat(result).isTrue() + } + + @Test + fun testIsWholeNumber_piIrrational_returnsFalse() { + val result = PI_REAL.isWholeNumber() + + assertThat(result).isFalse() + } + + @Test + fun testIsWholeNumber_threeIrrational_returnsFalse() { + val real = createIrrationalReal(3.0) + + val result = real.isWholeNumber() + + // Despite 3.0 being approximately a whole number, it isn't considered one since it's a double + // (and thus can have precision loss). + assertThat(result).isFalse() + } + @Test fun testIsNegative_default_throwsException() { val defaultReal = Real.getDefaultInstance() @@ -187,6 +297,139 @@ class RealExtensionsTest { assertThat(result).isTrue() } + @Test + fun testIsApproximatelyEqualTo_zeroAndOne_returnsFalse() { + val result = ZERO_REAL.isApproximatelyEqualTo(1.0) + + assertThat(result).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_twoAndTwoWithinThreshold_returnsTrue() { + val result = TWO_REAL.isApproximatelyEqualTo(2.0 + FLOAT_EQUALITY_INTERVAL / 2.0) + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_twoAndTwoOutsideThreshold_returnsFalse() { + val result = TWO_REAL.isApproximatelyEqualTo(2.0 + FLOAT_EQUALITY_INTERVAL * 2.0) + + assertThat(result).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_oneHalfAndOne_returnsFalse() { + val result = ONE_HALF_REAL.isApproximatelyEqualTo(1.0) + + assertThat(result).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_oneHalfAndPointFive_returnsTrue() { + val result = ONE_HALF_REAL.isApproximatelyEqualTo(0.5) + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_oneHalfAndPointSix_returnsFalse() { + val result = ONE_HALF_REAL.isApproximatelyEqualTo(0.6) + + assertThat(result).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_pointFiveAndPointFive_returnsTrue() { + val pointFiveReal = createIrrationalReal(0.5) + + val result = pointFiveReal.isApproximatelyEqualTo(0.5) + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_pointFiveAndPointSix_returnsFalse() { + val pointFiveReal = createIrrationalReal(0.5) + + val result = pointFiveReal.isApproximatelyEqualTo(0.6) + + assertThat(result).isFalse() + } + + @Test + fun testIsApproximatelyZero_default_throwsException() { + val defaultReal = Real.getDefaultInstance() + + val exception = assertThrows(IllegalStateException::class) { defaultReal.isApproximatelyZero() } + + assertThat(exception).hasMessageThat().contains("Invalid real") + } + + @Test + fun testIsApproximatelyZero_zeroInteger_returnsTrue() { + val result = ZERO_REAL.isApproximatelyZero() + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyZero_twoInteger_returnsFalse() { + val result = TWO_REAL.isApproximatelyZero() + + assertThat(result).isFalse() + } + + @Test + fun testIsApproximatelyZero_zeroFraction_returnsTrue() { + val real = createRationalReal(ZERO_FRACTION) + + val result = real.isApproximatelyZero() + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyZero_negativeZeroFraction_returnsTrue() { + val real = createRationalReal(-ZERO_FRACTION) + + val result = real.isApproximatelyZero() + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyZero_oneHalfFraction_returnsFalse() { + val result = ONE_HALF_REAL.isApproximatelyZero() + + assertThat(result).isFalse() + } + + @Test + fun testIsApproximatelyZero_zeroIrrational_returnsTrue() { + val real = createIrrationalReal(0.0) + + val result = real.isApproximatelyZero() + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyZero_irrationalCloseToZero_returnsTrue() { + val real = createIrrationalReal(0.000000001) + + val result = real.isApproximatelyZero() + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyZero_piIrrational_returnsFalse() { + val result = PI_REAL.isApproximatelyZero() + + assertThat(result).isFalse() + } + @Test fun testToDouble_default_returnsZeroDouble() { val defaultReal = Real.getDefaultInstance() @@ -239,135 +482,145 @@ class RealExtensionsTest { } @Test - fun testToPlainText_default_returnsEmptyString() { + fun testAsWholeNumber_default_throwsException() { val defaultReal = Real.getDefaultInstance() - val result = defaultReal.toPlainText() + val exception = assertThrows(IllegalStateException::class) { defaultReal.asWholeNumber() } - assertThat(result).isEmpty() + assertThat(exception).hasMessageThat().contains("Invalid real") } @Test - fun testToPlainText_twoInteger_returnsTwoString() { - val result = TWO_REAL.toPlainText() + fun testAsWholeNumber_twoInteger_returnsTwo() { + val result = TWO_REAL.asWholeNumber() - assertThat(result).isEqualTo("2") + assertThat(result).isEqualTo(2) } @Test - fun testToPlainText_negativeTwoInteger_returnsMinusTwoString() { - val result = NEGATIVE_TWO_REAL.toPlainText() + fun testAsWholeNumber_negativeTwoInteger_returnsNegativeTwo() { + val result = NEGATIVE_TWO_REAL.asWholeNumber() - assertThat(result).isEqualTo("-2") + assertThat(result).isEqualTo(-2) } @Test - fun testToPlainText_oneHalfFraction_returnsOneHalfString() { - val result = ONE_HALF_REAL.toPlainText() + fun testAsWholeNumber_oneHalfFraction_returnsNull() { + val result = ONE_HALF_REAL.asWholeNumber() - assertThat(result).isEqualTo("1/2") + assertThat(result).isNull() } @Test - fun testToPlainText_negativeOneHalfFraction_returnsMinusOneHalfString() { - val result = NEGATIVE_ONE_HALF_REAL.toPlainText() + fun testAsWholeNumber_threeOnesFraction_returnsNull() { + val result = THREE_ONES_REAL.asWholeNumber() - assertThat(result).isEqualTo("-1/2") + // 3/1 is treated as a fraction despite being numerically equivalent to a whole number. + assertThat(result).isNull() } @Test - fun testToPlainText_oneAndOneHalfFraction_returnsThreeHalvesString() { - val result = ONE_AND_ONE_HALF_REAL.toPlainText() + fun testAsWholeNumber_threeFraction_returnsThree() { + val result = THREE_FRACTION_REAL.asWholeNumber() - assertThat(result).isEqualTo("3/2") + assertThat(result).isEqualTo(3) } @Test - fun testToPlainText_negativeOneAndOneHalfFraction_returnsMinusThreeHalvesString() { - val result = NEGATIVE_ONE_AND_ONE_HALF_REAL.toPlainText() + fun testAsWholeNumber_negativeThreeFraction_returnsNegativeThree() { + val result = NEGATIVE_THREE_FRACTION_REAL.asWholeNumber() - assertThat(result).isEqualTo("-3/2") + assertThat(result).isEqualTo(-3) } @Test - fun testToPlainText_piIrrational_returnsPiString() { - val result = PI_REAL.toPlainText() + fun testAsWholeNumber_piIrrational_returnsNull() { + val result = PI_REAL.asWholeNumber() - assertThat(result).isEqualTo("3.1415") + assertThat(result).isNull() } @Test - fun testToPlainText_negativePiIrrational_returnsMinusPiString() { - val result = NEGATIVE_PI_REAL.toPlainText() + fun testAsWholeNumber_threeIrrational_returnsNull() { + val real = createIrrationalReal(3.0) - assertThat(result).isEqualTo("-3.1415") + val result = real.asWholeNumber() + + // Despite 3.0 being approximately a whole number, it isn't considered one since it's a double + // (and thus can have precision loss). + assertThat(result).isNull() } @Test - fun testIsApproximatelyEqualTo_zeroIntegerAndZero_returnsTrue() { - val result = ZERO_REAL.isApproximatelyEqualTo(0.0) + fun testToPlainText_default_returnsEmptyString() { + val defaultReal = Real.getDefaultInstance() - assertThat(result).isTrue() + val result = defaultReal.toPlainText() + + assertThat(result).isEmpty() } @Test - fun testIsApproximatelyEqualTo_zeroAndOne_returnsFalse() { - val result = ZERO_REAL.isApproximatelyEqualTo(1.0) + fun testToPlainText_twoInteger_returnsTwoString() { + val result = TWO_REAL.toPlainText() - assertThat(result).isFalse() + assertThat(result).isEqualTo("2") } @Test - fun testIsApproximatelyEqualTo_twoAndTwoWithinThreshold_returnsTrue() { - val result = TWO_REAL.isApproximatelyEqualTo(2.0 + FLOAT_EQUALITY_INTERVAL / 2.0) + fun testToPlainText_negativeTwoInteger_returnsMinusTwoString() { + val result = NEGATIVE_TWO_REAL.toPlainText() - assertThat(result).isTrue() + assertThat(result).isEqualTo("-2") } @Test - fun testIsApproximatelyEqualTo_twoAndTwoOutsideThreshold_returnsFalse() { - val result = TWO_REAL.isApproximatelyEqualTo(2.0 + FLOAT_EQUALITY_INTERVAL * 2.0) + fun testToPlainText_oneHalfFraction_returnsOneHalfString() { + val result = ONE_HALF_REAL.toPlainText() - assertThat(result).isFalse() + assertThat(result).isEqualTo("1/2") } @Test - fun testIsApproximatelyEqualTo_oneHalfAndOne_returnsFalse() { - val result = ONE_HALF_REAL.isApproximatelyEqualTo(1.0) + fun testToPlainText_negativeOneHalfFraction_returnsMinusOneHalfString() { + val result = NEGATIVE_ONE_HALF_REAL.toPlainText() - assertThat(result).isFalse() + assertThat(result).isEqualTo("-1/2") } @Test - fun testIsApproximatelyEqualTo_oneHalfAndPointFive_returnsTrue() { - val result = ONE_HALF_REAL.isApproximatelyEqualTo(0.5) + fun testToPlainText_oneAndOneHalfFraction_returnsThreeHalvesString() { + val result = ONE_AND_ONE_HALF_REAL.toPlainText() - assertThat(result).isTrue() + assertThat(result).isEqualTo("3/2") } @Test - fun testIsApproximatelyEqualTo_oneHalfAndPointSix_returnsFalse() { - val result = ONE_HALF_REAL.isApproximatelyEqualTo(0.6) + fun testToPlainText_negativeOneAndOneHalfFraction_returnsMinusThreeHalvesString() { + val result = NEGATIVE_ONE_AND_ONE_HALF_REAL.toPlainText() - assertThat(result).isFalse() + assertThat(result).isEqualTo("-3/2") } @Test - fun testIsApproximatelyEqualTo_pointFiveAndPointFive_returnsTrue() { - val pointFiveReal = createIrrationalReal(0.5) - - val result = pointFiveReal.isApproximatelyEqualTo(0.5) + fun testToPlainText_piIrrational_returnsPiString() { + val result = PI_REAL.toPlainText() - assertThat(result).isTrue() + assertThat(result).isEqualTo("3.1415") } @Test - fun testIsApproximatelyEqualTo_pointFiveAndPointSix_returnsFalse() { - val pointFiveReal = createIrrationalReal(0.5) + fun testToPlainText_negativePiIrrational_returnsMinusPiString() { + val result = NEGATIVE_PI_REAL.toPlainText() - val result = pointFiveReal.isApproximatelyEqualTo(0.6) + assertThat(result).isEqualTo("-3.1415") + } - assertThat(result).isFalse() + @Test + fun testIsApproximatelyEqualTo_zeroIntegerAndZero_returnsTrue() { + val result = ZERO_REAL.isApproximatelyEqualTo(0.0) + + assertThat(result).isTrue() } @Test @@ -1559,13 +1812,14 @@ class RealExtensionsTest { } @Test - fun testPow_negativeIntToOneHalfFraction_throwsException() { + fun testPow_negativeIntToOneHalfFraction_returnsNull() { val lhsReal = createIntegerReal(-3) val rhsReal = createRationalReal(ONE_HALF_FRACTION) - val exception = assertThrows(IllegalStateException::class) { lhsReal pow rhsReal } + val result = lhsReal pow rhsReal - assertThat(exception).hasMessageThat().contains("Radicand results in imaginary number") + // Cannot take the square root of a negative number. + assertThat(result).isNull() } @Test @@ -1579,23 +1833,25 @@ class RealExtensionsTest { } @Test - fun testPow_negativeFractionToOneHalfFraction_throwsException() { + fun testPow_negativeFractionToOneHalfFraction_returnsNull() { val lhsReal = NEGATIVE_ONE_AND_ONE_HALF_REAL val rhsReal = createRationalReal(ONE_HALF_FRACTION) - val exception = assertThrows(IllegalStateException::class) { lhsReal pow rhsReal } + val result = lhsReal pow rhsReal - assertThat(exception).hasMessageThat().contains("Radicand results in imaginary number") + // Cannot take the square root of a negative number. + assertThat(result).isNull() } @Test - fun testPow_negativeFractionToNegativeFractionWithOddNumerator_throwsException() { + fun testPow_negativeFractionToNegativeFractionWithOddNumerator_returnsNull() { val lhsReal = createRationalReal((-4).toWholeNumberFraction()) val rhsReal = createRationalReal(-ONE_AND_ONE_HALF_FRACTION) - val exception = assertThrows(IllegalStateException::class) { lhsReal pow rhsReal } + val result = lhsReal pow rhsReal - assertThat(exception).hasMessageThat().contains("Radicand results in imaginary number") + // Cannot take an even root of a negative number. + assertThat(result).isNull() } @Test @@ -1640,12 +1896,13 @@ class RealExtensionsTest { } @Test - fun testSqrt_negativeInteger_throwsException() { + fun testSqrt_negativeInteger_returnsNull() { val real = createIntegerReal(-2) - val exception = assertThrows(IllegalStateException::class) { sqrt(real) } + val result = sqrt(real) - assertThat(exception).hasMessageThat().contains("Radicand results in imaginary number") + // Cannot take square root of a negative number. + assertThat(result).isNull() } @Test @@ -1676,12 +1933,13 @@ class RealExtensionsTest { } @Test - fun testSqrt_negativeFraction_throwsException() { + fun testSqrt_negativeFraction_returnsNull() { val real = createRationalReal((-2).toWholeNumberFraction()) - val exception = assertThrows(IllegalStateException::class) { sqrt(real) } + val result = sqrt(real) - assertThat(exception).hasMessageThat().contains("Radicand results in imaginary number") + // Cannot take the square root of a negative number. + assertThat(result).isNull() } @Test From 06ba279040bf14d859d25d86865c7e8f15bed251 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 1 Feb 2022 22:13:40 -0800 Subject: [PATCH 089/134] KDocs + exemptions. Also, clean up polynomial sorting. --- .../file_content_validation_checks.textproto | 1 + .../android/util/math/ComparatorExtensions.kt | 27 ++- .../math/ExpressionToPolynomialConverter.kt | 50 ++++- .../android/util/math/FloatExtensions.kt | 7 +- .../android/util/math/FractionExtensions.kt | 6 +- .../util/math/MathExpressionExtensions.kt | 5 + .../android/util/math/PolynomialExtensions.kt | 144 ++++++++++----- .../oppia/android/util/math/RealExtensions.kt | 20 ++ .../util/math/ComparatorExtensionsTest.kt | 174 ++++++++++++++++++ 9 files changed, 378 insertions(+), 56 deletions(-) diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index bcae6e30b40..0c9432585fb 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -290,6 +290,7 @@ file_content_checks { exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt" + exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt" exempted_file_patterns: "testing/src/main/java/org/oppia/android/testing/junit/.+?\\.kt" } diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt index e853a03bfb9..52469413541 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt @@ -14,11 +14,32 @@ import com.google.protobuf.MessageLite * all of their items are equal per this [Comparator], including duplicates. */ fun Comparator.compareIterables(first: Iterable, second: Iterable): Int { + return compareIterablesInternal(first, second, reverseItemSort = false) +} + +/** + * Compares two [Iterable]s based on an item [Comparator] and returns the result, in much the same + * way as [compareIterables] except this reverses the result (that is, [first] will be considered + * less than [second] if it's larger). + * + * This should be used in place of a standard 'reversed()' since it will properly reverse (both the + * internal sorting and the comparison needs to be reversed in order for the reversal to be + * correct). + */ +fun Comparator.compareIterablesReversed(first: Iterable, second: Iterable): Int { + // Note that first & second are reversed here. + return compareIterablesInternal(second, first, reverseItemSort = true) +} + +private fun Comparator.compareIterablesInternal( + first: Iterable, second: Iterable, reverseItemSort: Boolean +): Int { // Reference: https://stackoverflow.com/a/30107086. - val firstIter = first.sortedWith(this).iterator() - val secondIter = second.sortedWith(this).iterator() + val itemComparator = if (reverseItemSort) reversed() else this + val firstIter = first.sortedWith(itemComparator).iterator() + val secondIter = second.sortedWith(itemComparator).iterator() while (firstIter.hasNext() && secondIter.hasNext()) { - val comparison = this.compare(firstIter.next(), secondIter.next()) + val comparison = this.compare(firstIter.next(), secondIter.next()).coerceIn(-1 .. 1) if (comparison != 0) return comparison // Found a different item. } diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt index e64aa1baa63..71a4c6c972a 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt @@ -25,15 +25,59 @@ import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperato import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator import org.oppia.android.app.model.Real +/** + * Converter from [MathExpression] to [Polynomial]. + * + * See the separate protos for specifics on structure, and [reduceToPolynomial] for the actual + * conversion function. + */ class ExpressionToPolynomialConverter private constructor() { companion object { - // TODO: document that this generally only relate to algebraic expressions. - fun MathExpression.reduceToPolynomial(): Polynomial? = - replaceSquareRoots() + /** + * Returns a new [Polynomial] that represents this [MathExpression], or null if it's not a valid + * polynomial. + * + * Polynomials are defined as a list of terms where each term has a coefficient and zero or more + * variables. There are a number of specific constraints that this function guarantees for all + * returned polynomials: + * - Terms will never have duplicate variable expressions (e.g. there will never be a returned + * polynomial with multiple 'x' terms, but there can be an 'x' and 'x^2' term). This is + * because effort is taken to combine like terms. + * - Terms are always sorted by lexicography of the variable names and variable powers which + * allows for comparison that operates independently of commutativity, associativity, and + * distributivity. + * - There will only ever be at most one constant term in the polynomial. + * - There will always be at least 1 term (even if it's the constant zero). + * - The polynomial will be mathematically equivalent to the original expression. + * - Coefficients will be kept to the highest possible precision (i.e. integers and fractions + * will be preferred over irrationals unless a rounding error occurs). + * - Most polynomial operations will be computed, including unary negation, addition, + * subtraction, multiplication (both implicit and explicit), division, and powers. + * + * Note that this will return null if a polynomial cannot be computed, such as in the cases: + * - The expression represents a division where the result has a remainder polynomial. + * - The expression results in a variable with a negative power or a division by an expression. + * - The expression results in a non-integer power (which includes a current limitation for + * expressions like 'sqrt(x)^2'; these cannot pass because internally the method cannot + * represent 'x^1/2'). + * - The expression results in a power variable (which can never represent a polynomial). + * - The expression is invalid (e.g. a default proto instance). + * + * This function is only expected to be used in conjunction with algebraic expressions. It's + * suggested to use evaluation when comparing for equivalence among numeric expressions as it + * should yield the same result and be more performant. + * + * The tests for this method provide very thorough and broad examples of different cases that + * this function supports. In particular, the equality tests are useful to see what sorts of + * expressions can be considered the same per [Polynomial] representation. + */ + fun MathExpression.reduceToPolynomial(): Polynomial? { + return replaceSquareRoots() .reduceToPolynomialAux() ?.removeUnnecessaryVariables() ?.simplifyRationals() ?.sort() + } private fun MathExpression.replaceSquareRoots(): MathExpression { return when (expressionTypeCase) { diff --git a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt index 27ac002c08c..96d072a669c 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt @@ -2,7 +2,12 @@ package org.oppia.android.util.math import kotlin.math.abs -/** The error margin used for approximately [Float] and [Double] equality checking. */ +/** + * The error margin used for approximating [Float] and [Double] equality checking, that is, the + * largest distance from any particular number before a new value will be considered unequal (i.e. + * all values between a float and (float-interval, float+interval) will be considered equal to the + * float). + */ const val FLOAT_EQUALITY_INTERVAL = 1e-5 /** diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index bd0c8093ede..8a762f4515a 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -10,8 +10,10 @@ fun Fraction.hasFractionalPart(): Boolean { } /** - * Returns whether this fraction only represents a whole number. Note that for the fraction '0' this - * will return true. + * Returns whether this fraction only represents a whole number. + * + * Note that for the fraction '0' this will return true. Furthermore, this will return false for + * whole number-like improper fractions such as '3/1'. */ fun Fraction.isOnlyWholeNumber(): Boolean { return !hasFractionalPart() diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 65d40515f5f..f537d3d00cf 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -47,4 +47,9 @@ fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() */ fun MathExpression.toComparableOperation(): ComparableOperation = convertToComparableOperation() +/** + * Returns the [Polynomial] representation of this [MathExpression]. + * + * See [reduceToPolynomial] for details. + */ fun MathExpression.toPolynomial(): Polynomial? = reduceToPolynomial() diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index e64f0aaabab..1b9f55d53c8 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -5,35 +5,15 @@ import org.oppia.android.app.model.Polynomial import org.oppia.android.app.model.Polynomial.Term import org.oppia.android.app.model.Polynomial.Term.Variable import org.oppia.android.app.model.Real -import java.util.SortedSet +/** Represents a single-term constant polynomial with the value of 0. */ val ZERO_POLYNOMIAL: Polynomial = createConstantPolynomial(ZERO) +/** Represents a single-term constant polynomial with the value of 1. */ val ONE_POLYNOMIAL: Polynomial = createConstantPolynomial(ONE) -// TODO: Kotlin-ify. -private val POLYNOMIAL_VARIABLE_COMPARATOR: Comparator by lazy { - // Note that power is reversed because larger powers should actually be sorted ahead of smaller - // powers for the same variable name (but variable name still takes precedence). This ensures - // cases like x^2y+y^2x are sorted in that order. - Comparator.comparing(Variable::getName).thenComparingReversed(Variable::getPower) -} - -private val POLYNOMIAL_TERM_COMPARATOR: Comparator by lazy { - // First, sort by all variable names to ensure xy is placed ahead of xz. Then, sort by variable - // powers in order of the variables (such that x^2y is ranked higher thank xy). Finally, sort by - // the coefficient to ensure equality through the comparator works correctly (though in practice - // like terms should always be combined). Note the specific reversing happening here. It's done in - // this way so that sorted set bigger/smaller list is reversed (which matches expectations since - // larger terms should appear earlier in the results). This is implementing an ordering similar to - // https://en.wikipedia.org/wiki/Polynomial#Definition, except for multi-variable functions (where - // variables of higher degree are preferred over lower degree by lexicographical order of variable - // names). - Comparator.comparing>( - { term -> term.variableList.toSortedSet(POLYNOMIAL_VARIABLE_COMPARATOR) }, - POLYNOMIAL_VARIABLE_COMPARATOR.reversed().toSetComparator() - ).reversed().thenComparing(Term::getCoefficient, REAL_COMPARATOR.reversed()) -} +private val POLYNOMIAL_VARIABLE_COMPARATOR by lazy { createVariableComparator() } +private val POLYNOMIAL_TERM_COMPARATOR by lazy { createTermComparator() } /** Returns whether this polynomial is a constant-only polynomial (contains no variables). */ fun Polynomial.isConstant(): Boolean = termCount == 1 && getTerm(0).variableCount == 0 @@ -63,6 +43,12 @@ fun Polynomial.toPlainText(): String { } } +/** + * Returns a version of this [Polynomial] with all zero-coefficient terms removed. + * + * This function guarantees that the returned polynomial have at least 1 term (even if it's just the + * constant zero). + */ fun Polynomial.removeUnnecessaryVariables(): Polynomial { return Polynomial.newBuilder().apply { addAllTerm( @@ -73,6 +59,14 @@ fun Polynomial.removeUnnecessaryVariables(): Polynomial { }.build().ensureAtLeastConstant() } +/** + * Returns a version of this [Polynomial] with all rational coefficients potentially simplified to + * integer terms. + * + * A rational coefficient can be simplified iff: + * - It has no fractional representation (which includes zero fraction cases). + * - It has a denominator of 1 (which represents a whole number, even for improper fractions). + */ fun Polynomial.simplifyRationals(): Polynomial { return Polynomial.newBuilder().apply { addAllTerm( @@ -85,6 +79,19 @@ fun Polynomial.simplifyRationals(): Polynomial { }.build() } +/** + * Returns a sorted version of this [Polynomial]. + * + * The returned version guarantees a repeatable and deterministic order that prioritizes variables + * earlier in the alphabet (or have lower lexicographical order), and have higher powers. Some + * examples: + * - 'x' will appear before '1'. + * - 'x^2' will appear before 'x'. + * - 'x' will appear before 'y'. + * - 'xy' will appear before 'x' and 'y'. + * - 'x^2y' will appear before 'xy^2', but after 'x^2y^2'. + * - 'xy^2' will appear before 'xy'. + */ fun Polynomial.sort(): Polynomial = Polynomial.newBuilder().apply { // The double sorting here is less efficient, but it ensures both terms and variables are // correctly kept sorted. Fortunately, most internal operations will keep variables sorted by @@ -99,6 +106,10 @@ fun Polynomial.sort(): Polynomial = Polynomial.newBuilder().apply { ) }.build() +/** + * Returns the negated version of this [Polynomial] such that the original polynomial plus the + * negative version would yield zero. + */ operator fun Polynomial.unaryMinus(): Polynomial { // Negating a polynomial just requires flipping the signs on all coefficients. return toBuilder() @@ -107,6 +118,14 @@ operator fun Polynomial.unaryMinus(): Polynomial { .build() } +/** + * Returns the sum of this [Polynomial] with [rhs]. + * + * The returned polynomial is guaranteed to: + * - Have all like terms combined. + * - Have simplified rational coefficients (per [simplifyRationals]. + * - Have no zero coefficients (unless the entire polynomial is zero, in which case just 1). + */ operator fun Polynomial.plus(rhs: Polynomial): Polynomial { // Adding two polynomials just requires combining their terms lists (taking into account combining // common terms). @@ -115,11 +134,25 @@ operator fun Polynomial.plus(rhs: Polynomial): Polynomial { }.build().combineLikeTerms().simplifyRationals().removeUnnecessaryVariables() } +/** + * Returns the subtraction of [rhs] from this [Polynomial]. + * + * The returned polynomial, when added with [rhs], will always equal the original polynomial. + * + * The returned polynomial has the same guarantee as those returned from [Polynomial.plus]. + */ operator fun Polynomial.minus(rhs: Polynomial): Polynomial { // a - b = a + -b return this + -rhs } +/** + * Returns the product of this [Polynomial] with [rhs]. + * + * This will correctly cross-multiply terms, for example: (1+x)*(1-x) will become 1-x^2. + * + * The returned polynomial has the same guarantee as those returned from [Polynomial.plus]. + */ operator fun Polynomial.times(rhs: Polynomial): Polynomial { // Polynomial multiplication is simply multiplying each term in one by each term in the other. val crossMultipliedTerms = termList.flatMap { leftTerm -> @@ -134,6 +167,15 @@ operator fun Polynomial.times(rhs: Polynomial): Polynomial { }.reduce(Polynomial::plus).simplifyRationals().removeUnnecessaryVariables() } +/** + * Returns the division of [rhs] from this [Polynomial], or null if there's a remainder after + * attempting the division. + * + * If this function returns non-null, it's guaranteed that the quotient times the divisor will yield + * the dividend. + * + * The returned polynomial has the same guarantee as those returned from [Polynomial.plus]. + */ operator fun Polynomial.div(rhs: Polynomial): Polynomial? { // See https://en.wikipedia.org/wiki/Polynomial_long_division#Pseudocode for reference. if (rhs.isApproximatelyZero()) { @@ -160,6 +202,17 @@ operator fun Polynomial.div(rhs: Polynomial): Polynomial? { return quotient.takeIf { remainder.isApproximatelyZero() } } +/** + * Returns the [Polynomial] that represents this [Polynomial] raised to [exp], or null if the result + * is not a valid polynomial or if a proper polynomial could not be kept along the way. + * + * This function will fail in a number of cases, including: + * - If [exp] is not a constant polynomial. + * - If this polynomial has more than one term (since that requires factoring). + * - If the result would yield a polynomial with a negative power. + * + * The returned polynomial has the same guarantee as those returned from [Polynomial.plus]. + */ infix fun Polynomial.pow(exp: Polynomial): Polynomial? { // Polynomial exponentiation is only supported if the right side is a constant polynomial, // otherwise the result cannot be a polynomial (though could still be compared to another @@ -415,28 +468,25 @@ private fun Real.maybeSimplifyRationalToInteger(): Real = when (realTypeCase) { null -> this } -// TODO: figure out of this can be removed. -private fun > Comparator.thenComparingReversed( - keySelector: (T) -> U -): Comparator = thenComparing(Comparator.comparing(keySelector).reversed()) - -// TODO: figure out of this can be removed. -private fun Comparator.toSetComparator(): Comparator> { - val itemComparator = this - return Comparator { first, second -> - // Reference: https://stackoverflow.com/a/30107086. - val firstIter = first.iterator() - val secondIter = second.iterator() - while (firstIter.hasNext() && secondIter.hasNext()) { - val comparison = itemComparator.compare(firstIter.next(), secondIter.next()) - if (comparison != 0) return@Comparator comparison // Found a different item. - } +private fun createTermComparator(): Comparator { + // First, sort by all variable names to ensure xy is placed ahead of xz. Then, sort by variable + // powers in order of the variables (such that x^2y is ranked higher thank xy). Finally, sort by + // the coefficient to ensure equality through the comparator works correctly (though in practice + // like terms should always be combined). Note the specific reversing happening here. It's done in + // this way so that sorted set bigger/smaller list is reversed (which matches expectations since + // larger terms should appear earlier in the results). This is implementing an ordering similar to + // https://en.wikipedia.org/wiki/Polynomial#Definition, except for multi-variable functions (where + // variables of higher degree are preferred over lower degree by lexicographical order of variable + // names). + val reversedVariableComparator = POLYNOMIAL_VARIABLE_COMPARATOR.reversed() + return compareBy>( + reversedVariableComparator::compareIterablesReversed, Term::getVariableList + ).thenByDescending(REAL_COMPARATOR, Term::getCoefficient) +} - // Everything is equal up to here, see if the lists are different length. - return@Comparator when { - firstIter.hasNext() -> 1 // The first list is longer, therefore "greater." - secondIter.hasNext() -> -1 // Ditto, but for the second list. - else -> 0 // Otherwise, they're the same length with all equal items (and are thus equal). - } - } +private fun createVariableComparator(): Comparator { + // Note that power is reversed because larger powers should actually be sorted ahead of smaller + // powers for the same variable name (but variable name still takes precedence). This ensures + // cases like x^2y+y^2x are sorted in that order. + return compareBy(Variable::getName).thenByDescending(Variable::getPower) } diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 25b520d5a3e..607277ed7ad 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -9,14 +9,17 @@ import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET import kotlin.math.absoluteValue import kotlin.math.pow +/** Represents an integer [Real] with value 0. */ val ZERO: Real by lazy { Real.newBuilder().apply { integer = 0 }.build() } +/** Represents an integer [Real] with value 1. */ val ONE: Real by lazy { Real.newBuilder().apply { integer = 1 }.build() } +/** Represents a rational fraction [Real] with value 1/2. */ val ONE_HALF: Real by lazy { Real.newBuilder().apply { rational = Fraction.newBuilder().apply { @@ -48,6 +51,12 @@ fun Real.isRational(): Boolean = realTypeCase == RATIONAL */ fun Real.isInteger(): Boolean = realTypeCase == INTEGER +/** + * Returns whether this [Real] is explicitly a whole number, that is, either an integer or a + * [Fraction] that's also a whole number. + * + * Note that this has the same limitations as [Fraction.isOnlyWholeNumber] for rational values. + */ fun Real.isWholeNumber(): Boolean { return when (realTypeCase) { RATIONAL -> rational.isOnlyWholeNumber() @@ -72,11 +81,14 @@ fun Real.isApproximatelyEqualTo(value: Double): Boolean { return toDouble().approximatelyEquals(value) } +/** Returns whether this [Real] is approximately zero per [Double.approximatelyEquals]. */ fun Real.isApproximatelyZero(): Boolean = isApproximatelyEqualTo(0.0) /** * Returns a [Double] representation of this [Real] that is approximately the same value (per * [isApproximatelyEqualTo]). + * + * This method throws an exception if this [Real] is invalid (such as a default proto instance). */ fun Real.toDouble(): Double { return when (realTypeCase) { @@ -87,6 +99,14 @@ fun Real.toDouble(): Double { } } +/** + * Returns the whole-number representation of this [Real], or null if there isn't one. + * + * This function should only be called if [isWholeNumber] returns true. The contract of that + * function guarantees that a non-null integer can be returned here for whole number reals. + * + * This method throws an exception if this [Real] is invalid (such as a default proto instance). + */ fun Real.asWholeNumber(): Int? { return when (realTypeCase) { RATIONAL -> if (rational.isOnlyWholeNumber()) rational.toWholeNumber() else null diff --git a/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt index 5c44c17968e..a1c73b45d71 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt @@ -204,6 +204,180 @@ class ComparatorExtensionsTest { assertThat(compareResult).isEqualTo(0) } + @Test + fun testCompareIterablesReversed_emptyList_emptyList_returnsZero() { + val leftList = listOf() + val rightList = listOf() + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(0) + } + + @Test + fun testCompareIterablesReversed_singletonList_emptyList_returnsNegativeOne() { + val leftList = listOf("1") + val rightList = listOf() + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterablesReversed_emptyList_singletonList_returnsOne() { + val leftList = listOf() + val rightList = listOf("1") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(1) + } + + @Test + fun testCompareIterablesReversed_singletonList_singletonList_sameElems_returnsZero() { + val leftList = listOf("1") + val rightList = listOf("1") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(0) + } + + @Test + fun testCompareIterablesReversed_twoItemList_singletonList_commonElem_returnsNegativeOne() { + val leftList = listOf("1", "2") + val rightList = listOf("1") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterablesReversed_singletonList_twoItemList_commonElem_returnsOne() { + val leftList = listOf("1") + val rightList = listOf("1", "2") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(1) + } + + @Test + fun testCompareIterablesReversed_equalSizeLists_sameItems_sameOrder_returnsZero() { + val leftList = listOf("1", "2") + val rightList = listOf("1", "2") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(0) + } + + @Test + fun testCompareIterablesReversed_equalSizeLists_sameItems_differentOrder_returnsZero() { + val leftList = listOf("1", "2") + val rightList = listOf("2", "1") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + // Order shouldn't matter. + assertThat(compareResult).isEqualTo(0) + } + + @Test + fun testCompareIterablesReversed_list223_list123_returnsNegativeOne() { + val leftList = listOf("2", "2", "3") + val rightList = listOf("1", "2", "3") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterablesReversed_list123_list223_returnsOne() { + val leftList = listOf("1", "2", "3") + val rightList = listOf("2", "2", "3") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(1) + } + + @Test + fun testCompareIterablesReversed_list123_list11_returnsNegativeOne() { + val leftList = listOf("1", "2", "3") + val rightList = listOf("1", "1") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterablesReversed_list123_list13_returnsNegativeOne() { + val leftList = listOf("1", "2", "3") + val rightList = listOf("1", "3") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterablesReversed_list223_list1_returnsNegativeOne() { + val leftList = listOf("1", "2", "3") + val rightList = listOf("1") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterablesReversed_list123_list2_returnsNegativeNegativeOne() { + val leftList = listOf("1", "2", "3") + val rightList = listOf("2") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterablesReversed_list22_list2_returnsNegativeOne() { + val leftList = listOf("2", "2") + val rightList = listOf("2") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + // The first list has an extra element. This also verifies that duplicates are correctly + // considered during comparison. + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterablesReversed_list2_list22_returnsOne() { + val leftList = listOf("2") + val rightList = listOf("2", "2") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + // The second list has an extra element. + assertThat(compareResult).isEqualTo(1) + } + + @Test + fun testCompareIterablesReversed_list22_list22_returnsZero() { + val leftList = listOf("2", "2") + val rightList = listOf("2", "2") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(0) + } + @Test fun testCompareProtos_defaultAndDefault_returnsZero() { val leftProto = TestMessage.newBuilder().build() From ad3091d5046c08666428235b722732b190093e06 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 1 Feb 2022 22:14:58 -0800 Subject: [PATCH 090/134] Lint fixes. --- .../org/oppia/android/util/math/ComparatorExtensions.kt | 6 ++++-- .../android/util/math/ExpressionToPolynomialConverter.kt | 4 ++-- .../oppia/android/util/math/MathExpressionExtensions.kt | 7 ------- .../org/oppia/android/util/math/PolynomialExtensions.kt | 5 +++-- .../util/math/ExpressionToPolynomialConverterTest.kt | 5 +++-- .../android/util/math/MathExpressionExtensionsTest.kt | 1 - .../oppia/android/util/math/PolynomialExtensionsTest.kt | 2 +- .../java/org/oppia/android/util/math/RealExtensionsTest.kt | 1 - 8 files changed, 13 insertions(+), 18 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt index 52469413541..c952e56686e 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt @@ -32,14 +32,16 @@ fun Comparator.compareIterablesReversed(first: Iterable, second: Itera } private fun Comparator.compareIterablesInternal( - first: Iterable, second: Iterable, reverseItemSort: Boolean + first: Iterable, + second: Iterable, + reverseItemSort: Boolean ): Int { // Reference: https://stackoverflow.com/a/30107086. val itemComparator = if (reverseItemSort) reversed() else this val firstIter = first.sortedWith(itemComparator).iterator() val secondIter = second.sortedWith(itemComparator).iterator() while (firstIter.hasNext() && secondIter.hasNext()) { - val comparison = this.compare(firstIter.next(), secondIter.next()).coerceIn(-1 .. 1) + val comparison = this.compare(firstIter.next(), secondIter.next()).coerceIn(-1..1) if (comparison != 0) return comparison // Found a different item. } diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt index 71a4c6c972a..3d487ff95b8 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt @@ -21,9 +21,9 @@ import org.oppia.android.app.model.MathUnaryOperation import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE import org.oppia.android.app.model.Polynomial +import org.oppia.android.app.model.Real import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator -import org.oppia.android.app.model.Real /** * Converter from [MathExpression] to [Polynomial]. @@ -66,7 +66,7 @@ class ExpressionToPolynomialConverter private constructor() { * This function is only expected to be used in conjunction with algebraic expressions. It's * suggested to use evaluation when comparing for equivalence among numeric expressions as it * should yield the same result and be more performant. - * + * * The tests for this method provide very thorough and broad examples of different cases that * this function supports. In particular, the equality tests are useful to see what sorts of * expressions can be considered the same per [Polynomial] representation. diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index f537d3d00cf..2924dc5418e 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -3,13 +3,6 @@ package org.oppia.android.util.math import org.oppia.android.app.model.ComparableOperation import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE import org.oppia.android.app.model.Polynomial import org.oppia.android.app.model.Real import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.convertToComparableOperation diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index 1b9f55d53c8..2b3dac6d421 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -188,8 +188,9 @@ operator fun Polynomial.div(rhs: Polynomial): Polynomial? { val divisorVariable = leadingDivisorTerm.highestDegreeVariable() val divisorVariableName = divisorVariable?.name val divisorDegree = leadingDivisorTerm.highestDegree() - while (!remainder.isApproximatelyZero() - && (remainder.getDegree() ?: return null) >= divisorDegree) { + while (!remainder.isApproximatelyZero() && + (remainder.getDegree() ?: return null) >= divisorDegree + ) { // Attempt to divide the leading terms (this may fail). Note that the leading term should always // be based on the divisor variable being used (otherwise subsequent division steps will be // inconsistent and potentially fail to resolve). diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialConverterTest.kt index 84d4f434731..bcf64991b55 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialConverterTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialConverterTest.kt @@ -1,7 +1,7 @@ package org.oppia.android.util.math -import com.google.common.truth.Truth.assertThat 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.app.model.MathExpression @@ -2310,7 +2310,8 @@ class ExpressionToPolynomialConverterTest { private companion object { private fun parseAlgebraicExpression( - expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + expression: String, + errorCheckingMode: ErrorCheckingMode = ALL_ERRORS ): MathExpression { return MathExpressionParser.parseAlgebraicExpression( expression, allowedVariables = listOf("x", "y", "z"), errorCheckingMode diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt index 8be3ee05bc3..7a52039d973 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt @@ -6,7 +6,6 @@ import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression -import org.oppia.android.testing.math.PolynomialSubject import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS diff --git a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt index ed4887261ff..36b1aaa5224 100644 --- a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt @@ -12,9 +12,9 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.robolectric.annotation.LooperMode -import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat /** Tests for [Polynomial] extensions. */ // FunctionName: test names are conventionally named with underscores. diff --git a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt index 978b4a014f9..842bd80f18c 100644 --- a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt @@ -10,7 +10,6 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized -import org.oppia.android.testing.math.RealSubject import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.robolectric.annotation.LooperMode From b8ce188c751c444f4ab569c7295aca0182513b6f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 2 Feb 2022 13:55:05 -0800 Subject: [PATCH 091/134] Post-merge fixes. Also, mark methods/classes that need tests. --- domain/BUILD.bazel | 2 +- ...AndInSimplestFormRuleClassifierProvider.kt | 4 +- ...putIsEquivalentToRuleClassifierProvider.kt | 4 +- ...ithUnitsIsEqualToRuleClassifierProvider.kt | 4 +- ...itsIsEquivalentToRuleClassifierProvider.kt | 4 +- ...putIsEquivalentToRuleClassifierProvider.kt | 21 ++++--- ...atchesExactlyWithRuleClassifierProvider.kt | 5 +- ...vialManipulationsRuleClassifierProvider.kt | 13 ++-- .../NumericExpressionInputModule.kt | 1 + ...umericInputEqualsRuleClassifierProvider.kt | 4 +- .../org/oppia/android/util/math/BUILD.bazel | 6 +- .../math/ComparableOperationExtensions.kt | 63 +++++++++++++++++++ .../math/ComparableOperationListExtensions.kt | 52 --------------- .../android/util/math/FloatExtensions.kt | 4 +- .../util/math/MathExpressionExtensions.kt | 19 +++--- .../android/util/math/MathExpressionParser.kt | 2 +- .../android/util/math/PolynomialExtensions.kt | 22 ++++--- .../oppia/android/util/math/RealExtensions.kt | 13 ++-- .../android/util/math/FloatExtensionsTest.kt | 22 +++---- 19 files changed, 144 insertions(+), 121 deletions(-) create mode 100644 utility/src/main/java/org/oppia/android/util/math/ComparableOperationExtensions.kt delete mode 100644 utility/src/main/java/org/oppia/android/util/math/ComparableOperationListExtensions.kt diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index 6c6bb620b5c..d40eceb2fd3 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -126,7 +126,7 @@ kt_android_library( "//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/math:parser", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", "//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 f9498f7d965..4a425174d31 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,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.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo import org.oppia.android.util.math.toDouble import org.oppia.android.util.math.toSimplestForm import javax.inject.Inject @@ -36,7 +36,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider input: Fraction, writtenTranslationContext: WrittenTranslationContext ): Boolean { - return answer.toDouble().approximatelyEquals(input.toDouble()) && + return answer.toDouble().isApproximatelyEqualTo(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 e2c42f7ec67..6846bd42652 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,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.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo 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.toDouble().approximatelyEquals(input.toDouble()) + return answer.toDouble().isApproximatelyEqualTo(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 9a225cc41ca..387022c4563 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.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo import javax.inject.Inject /** @@ -52,7 +52,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProvider @Inject constructor( } private fun realMatches(answer: Double, input: Double): Boolean { - return input.approximatelyEquals(answer) + return input.isApproximatelyEqualTo(answer) } private fun fractionMatches(answer: Fraction, input: Fraction): Boolean { 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 e94fc9191e7..594291c3c29 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,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.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo import org.oppia.android.util.math.toDouble import javax.inject.Inject @@ -41,7 +41,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProvider @Inject constructor( } // Verify the float version of the value for approximate comparison. - return extractRealValue(input).approximatelyEquals(extractRealValue(answer)) + return extractRealValue(input).isApproximatelyEqualTo(extractRealValue(answer)) } private fun extractRealValue(number: NumberWithUnits): Double { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt index 50094be6215..9e533395999 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -1,7 +1,8 @@ package org.oppia.android.domain.classify.rules.numericexpressioninput +import javax.inject.Inject import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.Polynomial +import org.oppia.android.app.model.Real import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier @@ -9,10 +10,10 @@ import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult -import org.oppia.android.util.math.toPolynomial -import javax.inject.Inject -import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.evaluateAsNumericExpression +import org.oppia.android.util.math.isApproximatelyEqualTo +// TODO: add tests. class NumericExpressionInputIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, private val consoleLogger: ConsoleLogger @@ -30,18 +31,18 @@ class NumericExpressionInputIsEquivalentToRuleClassifierProvider @Inject constru input: String, writtenTranslationContext: WrittenTranslationContext ): Boolean { - val answerExpression = parsePolynomial(answer) ?: return false - val inputExpression = parsePolynomial(input) ?: return false - return answerExpression.approximatelyEquals(inputExpression) + val answerValue = evaluateNumericExpression(answer) ?: return false + val inputValue = evaluateNumericExpression(input) ?: return false + return answerValue.isApproximatelyEqualTo(inputValue) } - private fun parsePolynomial(rawExpression: String): Polynomial? { + private fun evaluateNumericExpression(rawExpression: String): Real? { return when (val expResult = MathExpressionParser.parseNumericExpression(rawExpression)) { is MathParsingResult.Success -> { - expResult.result.toPolynomial().also { + expResult.result.evaluateAsNumericExpression().also { if (it == null) { consoleLogger.w( - "NumericExpEquivalent", "Expression is not a supported polynomial: $rawExpression." + "NumericExpEquivalent", "Expression failed to evaluate: $rawExpression." ) } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt index fe58ff2dca8..da2c5ce46c8 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -10,8 +10,9 @@ import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import javax.inject.Inject -import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo +// TODO: add tests. class NumericExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, private val consoleLogger: ConsoleLogger @@ -31,7 +32,7 @@ class NumericExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject con ): Boolean { val answerExpression = parseNumericExpression(answer) ?: return false val inputExpression = parseNumericExpression(input) ?: return false - return answerExpression.approximatelyEquals(inputExpression) + return answerExpression.isApproximatelyEqualTo(inputExpression) } private fun parseNumericExpression(rawExpression: String): MathExpression? { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index 5e17c0c4173..34c94f3f489 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -1,6 +1,6 @@ package org.oppia.android.domain.classify.rules.numericexpressioninput -import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.ComparableOperation import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier @@ -9,10 +9,11 @@ import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult -import org.oppia.android.util.math.toComparableOperationList +import org.oppia.android.util.math.toComparableOperation import javax.inject.Inject -import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo +// TODO: add tests. class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -33,12 +34,12 @@ class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvide ): Boolean { val answerExpression = parseComparableOperationList(answer) ?: return false val inputExpression = parseComparableOperationList(input) ?: return false - return answerExpression.approximatelyEquals(inputExpression) + return answerExpression.isApproximatelyEqualTo(inputExpression) } - private fun parseComparableOperationList(rawExpression: String): ComparableOperationList? { + private fun parseComparableOperationList(rawExpression: String): ComparableOperation? { return when (val expResult = MathExpressionParser.parseNumericExpression(rawExpression)) { - is MathParsingResult.Success -> expResult.result.toComparableOperationList() + is MathParsingResult.Success -> expResult.result.toComparableOperation() is MathParsingResult.Failure -> { consoleLogger.e( "NumericExpTrivialManips", diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt index ca42ea19de0..6a2cf9b50cb 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt @@ -7,6 +7,7 @@ import dagger.multibindings.StringKey import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.NumericExpressionInputRules +// TODO: add tests. /** Module that binds rule classifiers corresponding to the numeric expression input interaction. */ @Module class NumericExpressionInputModule { 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 2c7a6dc5212..9468881f022 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.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo import javax.inject.Inject /** @@ -30,5 +30,5 @@ class NumericInputEqualsRuleClassifierProvider @Inject constructor( answer: Double, input: Double, writtenTranslationContext: WrittenTranslationContext - ): Boolean = input.approximatelyEquals(answer) + ): Boolean = input.isApproximatelyEqualTo(answer) } 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 index 8fa89d72797..bc975383271 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -10,7 +10,7 @@ android_library( "//:oppia_api_visibility", ], exports = [ - ":comparable_operation_list_extensions", + ":comparable_operation_extensions", ":comparator_extensions", ":float_extensions", ":fraction_extensions", @@ -88,9 +88,9 @@ kt_android_library( ) kt_android_library( - name = "comparable_operation_list_extensions", + name = "comparable_operation_extensions", srcs = [ - "ComparableOperationListExtensions.kt", + "ComparableOperationExtensions.kt", ], deps = [ ":real_extensions", diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparableOperationExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparableOperationExtensions.kt new file mode 100644 index 00000000000..f66b5aeb8c8 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/ComparableOperationExtensions.kt @@ -0,0 +1,63 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.ComparableOperation +import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation +import org.oppia.android.app.model.ComparableOperation.ComparisonTypeCase.COMMUTATIVE_ACCUMULATION +import org.oppia.android.app.model.ComparableOperation.ComparisonTypeCase.COMPARISONTYPE_NOT_SET +import org.oppia.android.app.model.ComparableOperation.ComparisonTypeCase.CONSTANT_TERM +import org.oppia.android.app.model.ComparableOperation.ComparisonTypeCase.NON_COMMUTATIVE_OPERATION +import org.oppia.android.app.model.ComparableOperation.ComparisonTypeCase.VARIABLE_TERM +import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation +import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation.OperationTypeCase.EXPONENTIATION +import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation.OperationTypeCase.OPERATIONTYPE_NOT_SET +import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation.OperationTypeCase.SQUARE_ROOT + +// TODO: add tests. +/** + * Returns whether this [ComparableOperation] is approximately equal to another, that is, + * whether it exactly matches the other except for constants (which instead utilize + * [org.oppia.android.app.model.Real.isApproximatelyEqualTo]). + * + * This function assumes that both this [ComparableOperation] and [other] are sorted prior to + * equality checking. + */ +fun ComparableOperation.isApproximatelyEqualTo(other: ComparableOperation): Boolean { + return when { + isNegated != other.isNegated -> false + isInverted != other.isInverted -> false + comparisonTypeCase != other.comparisonTypeCase -> false + else -> when (comparisonTypeCase) { + COMMUTATIVE_ACCUMULATION -> + commutativeAccumulation.isApproximatelyEqualTo(other.commutativeAccumulation) + NON_COMMUTATIVE_OPERATION -> + nonCommutativeOperation.isApproximatelyEqualTo(other.nonCommutativeOperation) + CONSTANT_TERM -> constantTerm.isApproximatelyEqualTo(other.constantTerm) + VARIABLE_TERM -> variableTerm == other.variableTerm + COMPARISONTYPE_NOT_SET, null -> true + } + } +} + +private fun CommutativeAccumulation.isApproximatelyEqualTo( + other: CommutativeAccumulation +): Boolean { + if (accumulationType != other.accumulationType) return false + if (combinedOperationsCount != other.combinedOperationsCount) return false + return combinedOperationsList.zip(other.combinedOperationsList).all { (first, second) -> + first.isApproximatelyEqualTo(second) + } +} + +private fun NonCommutativeOperation.isApproximatelyEqualTo( + other: NonCommutativeOperation +): Boolean { + if (operationTypeCase != other.operationTypeCase) return false + return when (operationTypeCase) { + EXPONENTIATION -> { + exponentiation.leftOperand.isApproximatelyEqualTo(other.exponentiation.leftOperand) + && exponentiation.rightOperand.isApproximatelyEqualTo(other.exponentiation.rightOperand) + } + SQUARE_ROOT -> squareRoot.isApproximatelyEqualTo(other.squareRoot) + OPERATIONTYPE_NOT_SET, null -> true + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparableOperationListExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparableOperationListExtensions.kt deleted file mode 100644 index 32af4ff4acb..00000000000 --- a/utility/src/main/java/org/oppia/android/util/math/ComparableOperationListExtensions.kt +++ /dev/null @@ -1,52 +0,0 @@ -package org.oppia.android.util.math - -import org.oppia.android.app.model.ComparableOperationList -import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation -import org.oppia.android.app.model.ComparableOperationList.NonCommutativeOperation - -/** - * Returns whether this [ComparableOperationList] is approximately equal to another, that is, - * whether it exactly matches the other except for constants (which instead utilize - * [Real.approximatelyEquals]). - */ -fun ComparableOperationList.approximatelyEquals(other: ComparableOperationList): Boolean { - return rootOperation.approximatelyEquals(other.rootOperation) -} - -private fun ComparableOperation.approximatelyEquals(other: ComparableOperation): Boolean { - if (isNegated != other.isNegated) return false - if (isInverted != other.isInverted) return false - if (comparisonTypeCase != other.comparisonTypeCase) return false - return when (comparisonTypeCase) { - ComparableOperation.ComparisonTypeCase.COMMUTATIVE_ACCUMULATION -> - commutativeAccumulation.approximatelyEquals(other.commutativeAccumulation) - ComparableOperation.ComparisonTypeCase.NON_COMMUTATIVE_OPERATION -> - nonCommutativeOperation.approximatelyEquals(other.nonCommutativeOperation) - ComparableOperation.ComparisonTypeCase.CONSTANT_TERM -> - constantTerm.approximatelyEquals(other.constantTerm) - ComparableOperation.ComparisonTypeCase.VARIABLE_TERM -> variableTerm == other.variableTerm - ComparableOperation.ComparisonTypeCase.COMPARISONTYPE_NOT_SET, null -> true - } -} - -private fun CommutativeAccumulation.approximatelyEquals(other: CommutativeAccumulation): Boolean { - if (accumulationType != other.accumulationType) return false - if (combinedOperationsCount != other.combinedOperationsCount) return false - return combinedOperationsList.zip(other.combinedOperationsList).all { (first, second) -> - first.approximatelyEquals(second) - } -} - -private fun NonCommutativeOperation.approximatelyEquals(other: NonCommutativeOperation): Boolean { - if (operationTypeCase != other.operationTypeCase) return false - return when (operationTypeCase) { - NonCommutativeOperation.OperationTypeCase.EXPONENTIATION -> { - exponentiation.leftOperand.approximatelyEquals(other.exponentiation.leftOperand) - && exponentiation.rightOperand.approximatelyEquals(other.exponentiation.rightOperand) - } - NonCommutativeOperation.OperationTypeCase.SQUARE_ROOT -> - squareRoot.approximatelyEquals(other.squareRoot) - NonCommutativeOperation.OperationTypeCase.OPERATIONTYPE_NOT_SET, null -> true - } -} diff --git a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt index 96d072a669c..0be3d2ee3c3 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt @@ -14,14 +14,14 @@ const val FLOAT_EQUALITY_INTERVAL = 1e-5 * Returns whether this float approximately equals another based on a consistent epsilon value * ([FLOAT_EQUALITY_INTERVAL]). */ -fun Float.approximatelyEquals(other: Float): Boolean { +fun Float.isApproximatelyEqualTo(other: Float): Boolean { return abs(this - other) < FLOAT_EQUALITY_INTERVAL } /** Returns whether this double approximately equals another based on a consistent epsilon value * ([FLOAT_EQUALITY_INTERVAL]). */ -fun Double.approximatelyEquals(other: Double): Boolean { +fun Double.isApproximatelyEqualTo(other: Double): Boolean { return abs(this - other) < FLOAT_EQUALITY_INTERVAL } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 1c53036902b..14ff73f7bc2 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -54,31 +54,32 @@ fun MathExpression.toComparableOperation(): ComparableOperation = convertToCompa */ fun MathExpression.toPolynomial(): Polynomial? = reduceToPolynomial() +// TODO: add tests. /** * Returns whether this [MathExpression] approximately equals another, that is, that it fully * matches in its AST representation but all constants are compared using - * [Real.approximatelyEquals]. Further, this does not check parser markers when considering - * equivalence. + * [Real.isApproximatelyEqualTo]. Further, this does not check parser markers when considering + * equality. */ -fun MathExpression.approximatelyEquals(other: MathExpression): Boolean { +fun MathExpression.isApproximatelyEqualTo(other: MathExpression): Boolean { if (expressionTypeCase != other.expressionTypeCase) return false return when (expressionTypeCase) { - CONSTANT -> constant.approximatelyEquals(other.constant) + CONSTANT -> constant.isApproximatelyEqualTo(other.constant) VARIABLE -> variable == other.variable BINARY_OPERATION -> { binaryOperation.operator == other.binaryOperation.operator - && binaryOperation.leftOperand.approximatelyEquals(other.binaryOperation.leftOperand) - && binaryOperation.rightOperand.approximatelyEquals(other.binaryOperation.rightOperand) + && binaryOperation.leftOperand.isApproximatelyEqualTo(other.binaryOperation.leftOperand) + && binaryOperation.rightOperand.isApproximatelyEqualTo(other.binaryOperation.rightOperand) } UNARY_OPERATION -> { unaryOperation.operator == other.unaryOperation.operator - && unaryOperation.operand.approximatelyEquals(other.unaryOperation.operand) + && unaryOperation.operand.isApproximatelyEqualTo(other.unaryOperation.operand) } FUNCTION_CALL -> { functionCall.functionType == other.functionCall.functionType - && functionCall.argument.approximatelyEquals(other.functionCall.argument) + && functionCall.argument.isApproximatelyEqualTo(other.functionCall.argument) } - GROUP -> group.approximatelyEquals(other.group) + GROUP -> group.isApproximatelyEqualTo(other.group) EXPRESSIONTYPE_NOT_SET, null -> true } } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt index 20b44cee192..e85107864c8 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -1128,7 +1128,7 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo binaryOperation.operator == DIVIDE && binaryOperation.rightOperand.expressionTypeCase == CONSTANT && binaryOperation.rightOperand.constant - .toDouble().absoluteValue.approximatelyEquals(0.0) + .toDouble().absoluteValue.isApproximatelyEqualTo(0.0) } ?: binaryOperation.leftOperand.findNextDivisionByZero() ?: binaryOperation.rightOperand.findNextDivisionByZero() } diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index 4f76b82de58..6c41d9cd16f 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -18,20 +18,21 @@ private val POLYNOMIAL_TERM_COMPARATOR by lazy { createTermComparator() } /** Returns whether this polynomial is a constant-only polynomial (contains no variables). */ fun Polynomial.isConstant(): Boolean = termCount == 1 && getTerm(0).variableCount == 0 +// TODO: add tests. /** * Returns whether this [Polynomial] approximately equals an other, that is, that the polynomial has - * the exact same terms and approximately equal coefficients (see [Real.approximatelyEquals]). + * the exact same terms and approximately equal coefficients (see [Real.isApproximatelyEqualTo]). + * + * This function assumes that both this and the other [Polynomial] are sorted before checking for + * equality. */ -fun Polynomial.approximatelyEquals(other: Polynomial): Boolean { +fun Polynomial.isApproximatelyEqualTo(other: Polynomial): Boolean { if (termCount != other.termCount) return false // Terms can be zipped since they should be sorted prior to checking equivalence. - return termList.zip(other.termList).all { (first, second) -> first.approximatelyEquals(second) } -} - -private fun Term.approximatelyEquals(other: Term): Boolean { - // The variable lists can be exactly matched since they're sorted. - return coefficient.approximatelyEquals(other.coefficient) && variableList == other.variableList + return termList.zip(other.termList).all { (first, second) -> + first.isApproximatelyEqualTo(second) + } } /** @@ -296,6 +297,11 @@ private fun Polynomial.combineLikeTerms(): Polynomial { }.build().ensureAtLeastConstant() } +private fun Term.isApproximatelyEqualTo(other: Term): Boolean { + // The variable lists can be exactly matched since they're sorted. + return coefficient.isApproximatelyEqualTo(other.coefficient) && variableList == other.variableList +} + private fun Polynomial.pow(exp: Real): Polynomial? { val shouldBeInverted = exp.isNegative() val positivePower = if (shouldBeInverted) -exp else exp diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 06c29559363..9c451c65d34 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -73,23 +73,24 @@ fun Real.isNegative(): Boolean = when (realTypeCase) { REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $this.") } +// TODO: add tests. /** * Returns whether this [Real] approximately equals another, that is, if they evaluate to - * approximately the same value (see [Double.approximatelyEquals]). + * approximately the same value (see [Double.isApproximatelyEqualTo]). */ -fun Real.approximatelyEquals(other: Real): Boolean { - return isApproximatelyEqualTo(other.toDouble()) +fun Real.isApproximatelyEqualTo(other: Real): Boolean { + return this@isApproximatelyEqualTo.isApproximatelyEqualTo(other.toDouble()) } /** * Returns whether this [Real] is approximately equal to the specified [Double] per - * [Double.approximatelyEquals]. + * [Double.isApproximatelyEqualTo]. */ fun Real.isApproximatelyEqualTo(value: Double): Boolean { - return toDouble().approximatelyEquals(value) + return toDouble().isApproximatelyEqualTo(value) } -/** Returns whether this [Real] is approximately zero per [Double.approximatelyEquals]. */ +/** Returns whether this [Real] is approximately zero per [Double.isApproximatelyEqualTo]. */ fun Real.isApproximatelyZero(): Boolean = isApproximatelyEqualTo(0.0) /** diff --git a/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt index 6e1896902e6..c1fdc74d655 100644 --- a/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt @@ -18,7 +18,7 @@ class FloatExtensionsTest { val leftFloat = 0f val rightFloat = 0f - val result = leftFloat.approximatelyEquals(rightFloat) + val result = leftFloat.isApproximatelyEqualTo(rightFloat) assertThat(result).isTrue() } @@ -28,7 +28,7 @@ class FloatExtensionsTest { val leftFloat = 1.2f val rightFloat = 1.2f - val result = leftFloat.approximatelyEquals(rightFloat) + val result = leftFloat.isApproximatelyEqualTo(rightFloat) assertThat(result).isTrue() } @@ -38,7 +38,7 @@ class FloatExtensionsTest { val leftFloat = 1.2f val rightFloat = leftFloat + FLOAT_EQUALITY_INTERVAL.toFloat() / 10f - val result = leftFloat.approximatelyEquals(rightFloat) + val result = leftFloat.isApproximatelyEqualTo(rightFloat) // Verify that they are approximately equal, but not actually the same float. assertThat(result).isTrue() @@ -50,7 +50,7 @@ class FloatExtensionsTest { val leftFloat = 0f val rightFloat = 7.3f - val result = leftFloat.approximatelyEquals(rightFloat) + val result = leftFloat.isApproximatelyEqualTo(rightFloat) assertThat(result).isFalse() } @@ -60,7 +60,7 @@ class FloatExtensionsTest { val leftFloat = 1.2f val rightFloat = leftFloat + FLOAT_EQUALITY_INTERVAL.toFloat() * 2f - val result = leftFloat.approximatelyEquals(rightFloat) + val result = leftFloat.isApproximatelyEqualTo(rightFloat) assertThat(result).isFalse() } @@ -70,7 +70,7 @@ class FloatExtensionsTest { val leftFloat = 1.2f val rightFloat = 7.3f - val result = leftFloat.approximatelyEquals(rightFloat) + val result = leftFloat.isApproximatelyEqualTo(rightFloat) assertThat(result).isFalse() } @@ -80,7 +80,7 @@ class FloatExtensionsTest { val leftDouble = 0.0 val rightDouble = 0.0 - val result = leftDouble.approximatelyEquals(rightDouble) + val result = leftDouble.isApproximatelyEqualTo(rightDouble) assertThat(result).isTrue() } @@ -90,7 +90,7 @@ class FloatExtensionsTest { val leftDouble = 1.2 val rightDouble = 1.2 - val result = leftDouble.approximatelyEquals(rightDouble) + val result = leftDouble.isApproximatelyEqualTo(rightDouble) assertThat(result).isTrue() } @@ -100,7 +100,7 @@ class FloatExtensionsTest { val leftDouble = 1.2 val rightDouble = leftDouble + FLOAT_EQUALITY_INTERVAL / 10.0 - val result = leftDouble.approximatelyEquals(rightDouble) + val result = leftDouble.isApproximatelyEqualTo(rightDouble) // Verify that they are approximately equal, but not actually the same double. assertThat(result).isTrue() @@ -112,7 +112,7 @@ class FloatExtensionsTest { val leftDouble = 1.2 val rightDouble = leftDouble + FLOAT_EQUALITY_INTERVAL * 2 - val result = leftDouble.approximatelyEquals(rightDouble) + val result = leftDouble.isApproximatelyEqualTo(rightDouble) assertThat(result).isFalse() } @@ -122,7 +122,7 @@ class FloatExtensionsTest { val leftDouble = 1.2 val rightDouble = 7.3 - val result = leftDouble.approximatelyEquals(rightDouble) + val result = leftDouble.isApproximatelyEqualTo(rightDouble) assertThat(result).isFalse() } From 468b565377dfc25afa4fd6f8182a5f1690bd0a64 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 3 Feb 2022 20:56:39 -0800 Subject: [PATCH 092/134] Add extension tests. --- .../NumericExpressionInputModule.kt | 1 - .../rules/numericexpressioninput/BUILD.bazel | 99 +++ ...sEquivalentToRuleClassifierProviderTest.kt | 102 +++ ...esExactlyWithRuleClassifierProviderTest.kt | 115 +++ ...ManipulationsRuleClassifierProviderTest.kt | 104 +++ .../NumericExpressionInputModuleTest.kt | 106 +++ .../math/ComparableOperationExtensions.kt | 1 - .../util/math/MathExpressionExtensions.kt | 1 - .../android/util/math/PolynomialExtensions.kt | 33 +- .../oppia/android/util/math/RealExtensions.kt | 1 - .../org/oppia/android/util/math/BUILD.bazel | 22 +- .../math/ComparableOperationExtensionsTest.kt | 770 +++++++++++++++++ .../util/math/MathExpressionExtensionsTest.kt | 151 +++- .../util/math/PolynomialExtensionsTest.kt | 808 ++++++++++++++++++ .../android/util/math/RealExtensionsTest.kt | 288 +++++++ 15 files changed, 2576 insertions(+), 26 deletions(-) create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModuleTest.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/ComparableOperationExtensionsTest.kt diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt index 6a2cf9b50cb..ca42ea19de0 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt @@ -7,7 +7,6 @@ import dagger.multibindings.StringKey import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.NumericExpressionInputRules -// TODO: add tests. /** Module that binds rule classifiers corresponding to the numeric expression input interaction. */ @Module class NumericExpressionInputModule { diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel new file mode 100644 index 00000000000..a83a60b85cf --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel @@ -0,0 +1,99 @@ +""" +Tests for numeric expression input classifiers. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("//:oppia_android_test.bzl", "oppia_android_test") + +oppia_android_test( + name = "NumericExpressionInputIsEquivalentToRuleClassifierProviderTest", + srcs = ["NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt"], + custom_package = "org.oppia.android.domain.classify.rules.numericexpressioninput", + test_class = "org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputIsEquivalentToRuleClassifierProviderTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//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/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + +oppia_android_test( + name = "NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest", + srcs = ["NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt"], + custom_package = "org.oppia.android.domain.classify.rules.numericexpressioninput", + test_class = "org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//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/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + +oppia_android_test( + name = "NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest", + srcs = ["NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt"], + custom_package = "org.oppia.android.domain.classify.rules.numericexpressioninput", + test_class = "org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//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/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + +oppia_android_test( + name = "NumericExpressionInputModuleTest", + srcs = ["NumericExpressionInputModuleTest.kt"], + custom_package = "org.oppia.android.domain.classify.rules.numericexpressioninput", + test_class = "org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModuleTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//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/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + +dagger_rules() diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt new file mode 100644 index 00000000000..4f71926b3d4 --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt @@ -0,0 +1,102 @@ +package org.oppia.android.domain.classify.rules.numericexpressioninput + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import javax.inject.Inject +import javax.inject.Singleton +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.RuleClassifier +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.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode + +/** Tests for [NumericExpressionInputIsEquivalentToRuleClassifierProvider]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class NumericExpressionInputIsEquivalentToRuleClassifierProviderTest { + @Inject + internal lateinit var provider: NumericExpressionInputIsEquivalentToRuleClassifierProvider + + private lateinit var classifier: RuleClassifier + + @Before + fun setUp() { + setUpTestApplicationComponent() + classifier = provider.createRuleClassifier() + } + + // TODO: finish tests. + + @Test + fun test() { + val answerExpression = createMathExpression("0") + val inputExpression = createMathExpression("1") + + val matches = + classifier.matches( + answerExpression, + inputs = mapOf("x" to inputExpression), + writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + ) + + assertThat(matches).isTrue() + } + + private fun setUpTestApplicationComponent() { + DaggerNumericExpressionInputIsEquivalentToRuleClassifierProviderTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + + private fun createMathExpression(rawExpression: String) = InteractionObject.newBuilder().apply { + mathExpression = rawExpression + }.build() + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestDispatcherModule::class, LoggerModule::class, RobolectricModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(test: NumericExpressionInputIsEquivalentToRuleClassifierProviderTest) + } +} diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt new file mode 100644 index 00000000000..f69864d89b6 --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt @@ -0,0 +1,115 @@ +package org.oppia.android.domain.classify.rules.numericexpressioninput + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import javax.inject.Inject +import javax.inject.Singleton +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.RuleClassifier +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.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode + +/** Tests for [NumericExpressionInputMatchesExactlyWithRuleClassifierProvider]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest { + @Inject + internal lateinit var provider: NumericExpressionInputMatchesExactlyWithRuleClassifierProvider + + private lateinit var classifier: RuleClassifier + + @Before + fun setUp() { + setUpTestApplicationComponent() + classifier = provider.createRuleClassifier() + } + + // TODO: finish tests. + + // testMatches_zeroAnswer_zeroInput_returnsTrue + // testMatches_zeroAnswer_oneInput_returnsFalse + // testMatches_oneAnswer_zeroInput_returnsFalse + // testMatches_oneAnswer_oneInput_returnsTrue + // testMatches_answerAndInput_sameOperationParameters_returnsTrue + // testMatches_answerAndInput_same_returnsTrue + // testMatches_answerAndInput_differentByCommutativity_returnsFalse + // testMatches_answerAndInput_differentByAssociativity_returnsFalse + // testMatches_answerAndInput_differentByDistributivity_returnsFalse + // testMatches_answerAndInput_differentByNonCommutativeReordering_returnsFalse + // testMatches_answerAndInput_similarInMultipleWays_returnsTrue + // testMatches_answerAndInput_varyIncorrectlyInMultipleWays_returnsFalse + + @Test + fun test() { + val answerExpression = createMathExpression("0") + val inputExpression = createMathExpression("1") + + val matches = + classifier.matches( + answerExpression, + inputs = mapOf("x" to inputExpression), + writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + ) + + assertThat(matches).isTrue() + } + + private fun setUpTestApplicationComponent() { + DaggerNumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + + private fun createMathExpression(rawExpression: String) = InteractionObject.newBuilder().apply { + mathExpression = rawExpression + }.build() + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestDispatcherModule::class, LoggerModule::class, RobolectricModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(test: NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest) + } +} diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt new file mode 100644 index 00000000000..508a6243b80 --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt @@ -0,0 +1,104 @@ +package org.oppia.android.domain.classify.rules.numericexpressioninput + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import javax.inject.Inject +import javax.inject.Singleton +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.RuleClassifier +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.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode + +/** Tests for [NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest { + @Inject + internal lateinit var provider: NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider + + private lateinit var classifier: RuleClassifier + + @Before + fun setUp() { + setUpTestApplicationComponent() + classifier = provider.createRuleClassifier() + } + + // TODO: finish tests. + + @Test + fun test() { + val answerExpression = createMathExpression("0") + val inputExpression = createMathExpression("1") + + val matches = + classifier.matches( + answerExpression, + inputs = mapOf("x" to inputExpression), + writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + ) + + assertThat(matches).isTrue() + } + + private fun setUpTestApplicationComponent() { + DaggerNumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + + private fun createMathExpression(rawExpression: String) = InteractionObject.newBuilder().apply { + mathExpression = rawExpression + }.build() + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestDispatcherModule::class, LoggerModule::class, RobolectricModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject( + test: NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest + ) + } +} diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModuleTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModuleTest.kt new file mode 100644 index 00000000000..3ad0eed220e --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModuleTest.kt @@ -0,0 +1,106 @@ +package org.oppia.android.domain.classify.rules.numericexpressioninput + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import javax.inject.Inject +import javax.inject.Singleton +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.NumericExpressionInputRules +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.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode + +/** Tests for [NumericExpressionInputModule]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class NumericExpressionInputModuleTest { + @Inject + @NumericExpressionInputRules + lateinit var numericExpressionInputClassifiers: Map + + @Before + fun setUp() { + setUpTestApplicationComponent() + } + + @Test + fun testModule_hasAtLeastOneClassifier() { + assertThat(numericExpressionInputClassifiers).isNotEmpty() + } + + @Test + fun testModule_hasNoDuplicateClassifiers() { + assertThat(numericExpressionInputClassifiers.values.toSet()).hasSize( + numericExpressionInputClassifiers.size + ) + } + + @Test + fun testModule_providesMatchesExactlyWithClassifier() { + assertThat(numericExpressionInputClassifiers).containsKey("MatchesExactlyWith") + } + + @Test + fun testModule_providesMatchesUpToTrivialManipulationsClassifier() { + assertThat(numericExpressionInputClassifiers).containsKey("MatchesUpToTrivialManipulations") + } + + @Test + fun testModule_providesIsEquivalentToClassifier() { + assertThat(numericExpressionInputClassifiers).containsKey("MatchesExactlyWith") + } + + private fun setUpTestApplicationComponent() { + DaggerNumericExpressionInputModuleTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestDispatcherModule::class, LoggerModule::class, RobolectricModule::class, + NumericExpressionInputModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(test: NumericExpressionInputModuleTest) + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparableOperationExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparableOperationExtensions.kt index f66b5aeb8c8..249bbea8394 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ComparableOperationExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ComparableOperationExtensions.kt @@ -12,7 +12,6 @@ import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation.O import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation.OperationTypeCase.OPERATIONTYPE_NOT_SET import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation.OperationTypeCase.SQUARE_ROOT -// TODO: add tests. /** * Returns whether this [ComparableOperation] is approximately equal to another, that is, * whether it exactly matches the other except for constants (which instead utilize diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 14ff73f7bc2..ab2cdd1d5c2 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -54,7 +54,6 @@ fun MathExpression.toComparableOperation(): ComparableOperation = convertToCompa */ fun MathExpression.toPolynomial(): Polynomial? = reduceToPolynomial() -// TODO: add tests. /** * Returns whether this [MathExpression] approximately equals another, that is, that it fully * matches in its AST representation but all constants are compared using diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index 6c41d9cd16f..b1d215cd436 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -18,23 +18,6 @@ private val POLYNOMIAL_TERM_COMPARATOR by lazy { createTermComparator() } /** Returns whether this polynomial is a constant-only polynomial (contains no variables). */ fun Polynomial.isConstant(): Boolean = termCount == 1 && getTerm(0).variableCount == 0 -// TODO: add tests. -/** - * Returns whether this [Polynomial] approximately equals an other, that is, that the polynomial has - * the exact same terms and approximately equal coefficients (see [Real.isApproximatelyEqualTo]). - * - * This function assumes that both this and the other [Polynomial] are sorted before checking for - * equality. - */ -fun Polynomial.isApproximatelyEqualTo(other: Polynomial): Boolean { - if (termCount != other.termCount) return false - - // Terms can be zipped since they should be sorted prior to checking equivalence. - return termList.zip(other.termList).all { (first, second) -> - first.isApproximatelyEqualTo(second) - } -} - /** * Returns the first term coefficient from this polynomial. This corresponds to the whole value of * the polynomial iff isConstant() returns true, otherwise this value isn't useful. @@ -123,6 +106,22 @@ fun Polynomial.sort(): Polynomial = Polynomial.newBuilder().apply { ) }.build() +/** + * Returns whether this [Polynomial] approximately equals an other, that is, that the polynomial has + * the exact same terms and approximately equal coefficients (see [Real.isApproximatelyEqualTo]). + * + * This function assumes that both this and the other [Polynomial] are sorted before checking for + * equality (i.e. via [sort]). + */ +fun Polynomial.isApproximatelyEqualTo(other: Polynomial): Boolean { + if (termCount != other.termCount) return false + + // Terms can be zipped since they should be sorted prior to checking equivalence. + return termList.zip(other.termList).all { (first, second) -> + first.isApproximatelyEqualTo(second) + } +} + /** * Returns the negated version of this [Polynomial] such that the original polynomial plus the * negative version would yield zero. diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 9c451c65d34..344a5f95273 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -73,7 +73,6 @@ fun Real.isNegative(): Boolean = when (realTypeCase) { REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $this.") } -// TODO: add tests. /** * Returns whether this [Real] approximately equals another, that is, if they evaluate to * approximately the same value (see [Double.isApproximatelyEqualTo]). 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 index 7a7aed1e686..f3ebddf37d7 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -42,6 +42,24 @@ oppia_android_test( ], ) +oppia_android_test( + name = "ComparableOperationExtensionsTest", + srcs = ["ComparableOperationExtensionsTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.ComparableOperationExtensionsTest", + 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", + "//utility/src/main/java/org/oppia/android/util/math:fraction_parser", + ], +) + oppia_android_test( name = "ComparatorExtensionsTest", srcs = ["ComparatorExtensionsTest.kt"], @@ -178,9 +196,11 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", "//testing/src/main/java/org/oppia/android/testing/math:real_subject", - "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", diff --git a/utility/src/test/java/org/oppia/android/util/math/ComparableOperationExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/ComparableOperationExtensionsTest.kt new file mode 100644 index 00000000000..0e277196fa3 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/ComparableOperationExtensionsTest.kt @@ -0,0 +1,770 @@ +package org.oppia.android.util.math + +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.app.model.ComparableOperation +import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation +import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation +import org.oppia.android.app.model.Real +import org.robolectric.annotation.LooperMode + +/** Tests for [ComparableOperation] extensions. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class ComparableOperationExtensionsTest { + private val fractionParser by lazy { FractionParser() } + + @Test + fun testIsApproximatelyEqualTo_firstIsDefault_secondIsDefault_returnTrue() { + val first = ComparableOperation.getDefaultInstance() + val second = ComparableOperation.getDefaultInstance() + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsDefault_secondIsConstantInt2_returnFalse() { + val first = ComparableOperation.getDefaultInstance() + val second = createConstantOp(constant = 2) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstantInt2_secondIsConstantInt2_bothOrders_returnTrue() { + val first = createConstantOp(constant = 2) + val second = createConstantOp(constant = 2) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstantInt2_secondIsConstantInt3_returnsFalse() { + val first = createConstantOp(constant = 2) + val second = createConstantOp(constant = 3) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstantInt2_secondIsConstantFraction2_returnsTrue() { + val first = createConstantOp(constant = 2) + val second = createConstantOp(constantFraction = "2") + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstInt2_secondIsConstFraction3Halves_returnsFalse() { + val first = createConstantOp(constant = 2) + val second = createConstantOp(constantFraction = "3/2") + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstantInt3_secondIsConstantFraction3Ones_returnsTrue() { + val first = createConstantOp(constant = 3) + val second = createConstantOp(constantFraction = "3/1") + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstantInt2_secondIsConstantFraction3_returnsFalse() { + val first = createConstantOp(constant = 2) + val second = createConstantOp(constantFraction = "3") + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstantInt2_secondIsConstantDouble2_returnsTrue() { + val first = createConstantOp(constant = 2) + val second = createConstantOp(constant = 2.0) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstInt2_secondIsConstDouble2PlusMargin_returnsTrue() { + val first = createConstantOp(constant = 2) + val second = createConstantOp(constant = 2.0000001) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstantInt3_secondIsConstantPi_returnsFalse() { + val first = createConstantOp(constant = 3) + val second = createConstantOp(constant = 3.14) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsDoubleOnePointFive_secondIsFracThreeHalves_returnsTrue() { + val first = createConstantOp(constant = 1.5) + val second = createConstantOp(constantFraction = "3/2") + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsVariableX_secondIsVariableX_returnsTrue() { + val first = createVariableOp(name = "x") + val second = createVariableOp(name = "x") + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsVariableX_secondIsVariableY_returnsFalse() { + val first = createVariableOp(name = "x") + val second = createVariableOp(name = "y") + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsNegatedInt2_secondIsNegatedInt2_returnsTrue() { + val first = createConstantOp(constant = 2).toNegated() + val second = createConstantOp(constant = 2).toNegated() + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsNegatedInt2_secondIsNotNegatedInt2_returnsFalse() { + val first = createConstantOp(constant = 2).toNegated() + val second = createConstantOp(constant = 2) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsInvertedInt2_secondIsInvertedInt2_returnsTrue() { + val first = createConstantOp(constant = 2).toInverted() + val second = createConstantOp(constant = 2).toInverted() + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsInvertedInt2_secondIsNotInvertedInt2_returnsFalse() { + val first = createConstantOp(constant = 2).toInverted() + val second = createConstantOp(constant = 2) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstantInt2_secondIsSumOfInt2And3_returnFalse() { + val first = createConstantOp(constant = 2) + val second = createSumOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsVariableX_secondIsSumOfInt2And3_returnFalse() { + val first = createVariableOp(name = "x") + val second = createSumOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstantInt2_secondIsProductOfInt2And3_returnFalse() { + val first = createConstantOp(constant = 2) + val second = createProductOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsVariableX_secondIsProductOfInt2And3_returnFalse() { + val first = createVariableOp(name = "x") + val second = createProductOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsSumOfInt2And3_secondIsSumOfInt2And3_returnsTrue() { + val first = createSumOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + val second = createSumOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsSumOfInt2And3_secondIsSumOfInt3And2_returnsFalse() { + val first = createSumOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + val second = createSumOp( + createConstantOp(constant = 3), + createConstantOp(constant = 2) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // Order matters. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsSumOfInt2And3_secondIsProductOfInt2And3_returnsFalse() { + val first = createSumOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + val second = createProductOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // The accumulation type must match. Since this check is symmetric, it's also verifying the case + // when the left-hand side is a product. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsProductOfInt2And3_secondIsProductOfInt2And3_returnsTrue() { + val first = createProductOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + val second = createProductOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsProductOfInt2And3_secondIsProductOfInt3And2_returnsFalse() { + val first = createProductOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + val second = createProductOp( + createConstantOp(constant = 3), + createConstantOp(constant = 2) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // Order matters. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstantInt2_secondIsSquareRootOfInt2_returnsFalse() { + val first = createConstantOp(constant = 2) + val second = createSquareRootOp(arg = createConstantOp(constant = 2)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsVariableX_secondIsSquareRootOfIntX_returnsFalse() { + val first = createVariableOp(name = "x") + val second = createSquareRootOp(arg = createVariableOp(name = "x")) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsSumOfInt2And3_secondIsSquareRootOfInt2_returnsFalse() { + val first = createSumOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + val second = createSquareRootOp(arg = createConstantOp(constant = 2)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsProductOfInt2And3_secondIsSquareRootOfInt2_returnsFalse() { + val first = createProductOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + val second = createSquareRootOp(arg = createConstantOp(constant = 2)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstantInt2_secondIsExpOfXAnd2_returnsFalse() { + val first = createConstantOp(constant = 2) + val second = createExpOp( + lhs = createVariableOp(name = "x"), + rhs = createConstantOp(constant = 2) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsVariableX_secondIsExpOfXAnd2_returnsFalse() { + val first = createVariableOp(name = "x") + val second = createExpOp( + lhs = createVariableOp(name = "x"), + rhs = createConstantOp(constant = 2) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsSumOfInt2And3_secondIsExpOfInt2And3_returnsFalse() { + val first = createSumOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + val second = createExpOp( + lhs = createConstantOp(constant = 2), + rhs = createConstantOp(constant = 3) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsProductOfInt2And3_secondIsExpOfInt2And3_returnsFalse() { + val first = createProductOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + val second = createExpOp( + lhs = createConstantOp(constant = 2), + rhs = createConstantOp(constant = 3) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsExpOfXAnd2_secondIsExpOfXAnd2_returnsTrue() { + val first = createExpOp( + lhs = createVariableOp(name = "x"), + rhs = createConstantOp(constant = 2) + ) + val second = createExpOp( + lhs = createVariableOp(name = "x"), + rhs = createConstantOp(constant = 2) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsExpOfXAnd2_secondIsExpOfXAnd3_returnsFalse() { + val first = createExpOp( + lhs = createVariableOp(name = "x"), + rhs = createConstantOp(constant = 2) + ) + val second = createExpOp( + lhs = createVariableOp(name = "x"), + rhs = createConstantOp(constant = 3) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsExpOfXAnd2_secondIsExpOfYAnd2_returnsFalse() { + val first = createExpOp( + lhs = createVariableOp(name = "x"), + rhs = createConstantOp(constant = 2) + ) + val second = createExpOp( + lhs = createVariableOp(name = "y"), + rhs = createConstantOp(constant = 2) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsExpOfInt2AndThree_secondIsSquareRootOfInt2_returnsFalse() { + val first = createExpOp( + lhs = createConstantOp(constant = 2), + rhs = createConstantOp(constant = 3) + ) + val second = createSquareRootOp(arg = createConstantOp(constant = 2)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsExpOfInt2AndOneHalf_secondIsSqRootOfInt2_returnsFalse() { + val first = createExpOp( + lhs = createConstantOp(constant = 2), + rhs = createConstantOp(constantFraction = "1/2") + ) + val second = createSquareRootOp(arg = createConstantOp(constant = 2)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // The two expressions are technically numerically equal, but they don't pass the equality check + // for comparable operations since exponentiation and square roots aren't simplified. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsSquareRootOfInt2_secondIsSquareRootOfInt2_returnsTrue() { + val first = createSquareRootOp(arg = createConstantOp(constant = 2)) + val second = createSquareRootOp(arg = createConstantOp(constant = 2)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsSquareRootOfInt2_secondIsSquareRootOfInt3_returnsFalse() { + val first = createSquareRootOp(arg = createConstantOp(constant = 2)) + val second = createSquareRootOp(arg = createConstantOp(constant = 3)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_fullOperation_withNesting_allMatching_returnsTrue() { + val complexOp = createSumOp( + createProductOp( + createSumOp( + createVariableOp(name = "x"), + createConstantOp(constant = 3.14) + ), + createExpOp( + lhs = createConstantOp(constant = 3).toNegated(), + rhs = createSquareRootOp(arg = createConstantOp(3)) + ).toInverted() + ) + ) + + val result = complexOp.isApproximatelyEqualTo(complexOp) + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_fullOperation_withNesting_innerDifference_returnsFalse() { + val first = createSumOp( + createProductOp( + createSumOp( + createVariableOp(name = "x"), + createConstantOp(constant = 3.14) + ), + createExpOp( + lhs = createConstantOp(constant = 3).toNegated(), + rhs = createSquareRootOp(arg = createConstantOp(3)) + ).toInverted() + ) + ) + val second = createSumOp( + createProductOp( + createSumOp( + createVariableOp(name = "x"), + createConstantOp(constant = 3.14) + ), + createExpOp( + lhs = createConstantOp(constant = 2).toNegated(), + rhs = createSquareRootOp(arg = createConstantOp(3)) + ).toInverted() + ) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_fullOperation_comparedToDefault_returnsFalse() { + val first = createSumOp( + createProductOp( + createSumOp( + createVariableOp(name = "x"), + createConstantOp(constant = 3.14) + ), + createExpOp( + lhs = createConstantOp(constant = 3).toNegated(), + rhs = createSquareRootOp(arg = createConstantOp(3)) + ).toInverted() + ) + ) + val second = ComparableOperation.getDefaultInstance() + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + private fun createConstantOp(constant: Int) = ComparableOperation.newBuilder().apply { + constantTerm = createIntegerReal(constant) + }.build() + + private fun createConstantOp(constantFraction: String) = ComparableOperation.newBuilder().apply { + constantTerm = createRationalReal(rawFractionExpression = constantFraction) + }.build() + + private fun createConstantOp(constant: Double) = ComparableOperation.newBuilder().apply { + constantTerm = createIrrationalReal(constant) + }.build() + + private fun createVariableOp(name: String) = ComparableOperation.newBuilder().apply { + variableTerm = name + }.build() + + private fun createSumOp( + vararg ops: ComparableOperation + ) = ComparableOperation.newBuilder().apply { + commutativeAccumulation = CommutativeAccumulation.newBuilder().apply { + accumulationType = CommutativeAccumulation.AccumulationType.SUMMATION + addAllCombinedOperations(ops.asIterable()) + }.build() + }.build() + + private fun createProductOp( + vararg ops: ComparableOperation + ) = ComparableOperation.newBuilder().apply { + commutativeAccumulation = CommutativeAccumulation.newBuilder().apply { + accumulationType = CommutativeAccumulation.AccumulationType.PRODUCT + addAllCombinedOperations(ops.asIterable()) + }.build() + }.build() + + private fun createSquareRootOp( + arg: ComparableOperation + ) = ComparableOperation.newBuilder().apply { + nonCommutativeOperation = NonCommutativeOperation.newBuilder().apply { + squareRoot = arg + }.build() + }.build() + + private fun createExpOp( + lhs: ComparableOperation, rhs: ComparableOperation + ) = ComparableOperation.newBuilder().apply { + nonCommutativeOperation = NonCommutativeOperation.newBuilder().apply { + exponentiation = NonCommutativeOperation.BinaryOperation.newBuilder().apply { + leftOperand = lhs + rightOperand = rhs + }.build() + }.build() + }.build() + + private fun ComparableOperation.toNegated() = toBuilder().apply { + isNegated = true + }.build() + + private fun ComparableOperation.toInverted() = toBuilder().apply { + isInverted = true + }.build() + + private fun createIntegerReal(value: Int) = Real.newBuilder().apply { + integer = value + }.build() + + private fun createRationalReal(rawFractionExpression: String) = Real.newBuilder().apply { + rational = fractionParser.parseFractionFromString(rawFractionExpression) + }.build() + + private fun createIrrationalReal(value: Double) = Real.newBuilder().apply { + irrational = value + }.build() +} diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt index 7a52039d973..abc2888f94e 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt @@ -1,14 +1,21 @@ package org.oppia.android.util.math -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat +import java.lang.IllegalStateException import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression +import org.oppia.android.testing.assertThrows +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat import org.oppia.android.testing.math.RealSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.REQUIRED_ONLY import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression import org.oppia.android.util.math.MathExpressionParser.Companion.parseNumericExpression @@ -27,9 +34,12 @@ import org.robolectric.annotation.LooperMode // FunctionName: test names are conventionally named with underscores. // SameParameterValue: tests should have specific context included/excluded for readability. @Suppress("FunctionName", "SameParameterValue") -@RunWith(AndroidJUnit4::class) +@RunWith(OppiaParameterizedTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class MathExpressionExtensionsTest { + @Parameter lateinit var exp1: String + @Parameter lateinit var exp2: String + @Test fun testToRawLatex_algebraicExpression_divNotAsFraction_returnsLatexStringWithDivision() { val expression = parseAlgebraicExpression("(x^2+7x-y)/2") @@ -134,14 +144,147 @@ class MathExpressionExtensionsTest { } } + /* Equality checks. Note that these are symmetrical to reduce the number of needed test cases. */ + + @Test + fun testIsApproximatelyEqualTo_oneIsDefault_otherIsConstInt2_returnsFalse() { + val first = MathExpression.getDefaultInstance() + val second = parseNumericExpression("2") + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_oneIsConstInt2_otherIsDefault_returnsFalse() { + val first = parseNumericExpression("2") + val second = MathExpression.getDefaultInstance() + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + @RunParameterized( + Iteration("2==2", "exp1=2", "exp2=2"), + Iteration("2==2.000000001", "exp1=2", "exp2=2.000000001"), + Iteration("x+1==x+1", "exp1=x+1", "exp2=x+1"), + Iteration("x-1==x-1", "exp1=x-1", "exp2=x-1"), + Iteration("x*2==x*2", "exp1=x*2", "exp2=x*2"), + Iteration("x/2==x/2", "exp1=x/2", "exp2=x/2"), + Iteration("x^2==x^2", "exp1=x^2", "exp2=x^2"), + Iteration("-x==-x", "exp1=-x", "exp2=-x"), + Iteration("sqrt(x)==sqrt(x)", "exp1=sqrt(x)", "exp2=sqrt(x)") + ) + fun testIsApproximatelyEqualTo_bothAreSingleTermsOrOperations_andSame_returnsTrue() { + val first = parseAlgebraicExpression(exp1) + val second = parseAlgebraicExpression(exp2) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + @RunParameterized( + Iteration("2!=3", "exp1=2", "exp2=3"), + Iteration("2!=3/2", "exp1=2", "exp2=3/2"), + Iteration("2!=3.14", "exp1=2", "exp2=3.14"), + Iteration("x!=y", "exp1=x", "exp2=y"), + Iteration("x!=2", "exp1=x", "exp2=2"), + // The number of terms must match. + Iteration("1+x!=1", "exp1=1+x", "exp2=1"), + Iteration("1+x!=x", "exp1=1+x", "exp2=x"), + Iteration("1+1+x!=2+x", "exp1=1+1+x", "exp2=2+x"), + // Term order must match. + Iteration("1+x!=2+x", "exp1=1+x", "exp2=2+x"), + Iteration("1+x!=x+1", "exp1=1+x", "exp2=x+1"), + Iteration("1-x!=2-x", "exp1=1-x", "exp2=2-x"), + Iteration("1-x!=x-1", "exp1=1-x", "exp2=x-1"), + Iteration("2*x!=3*x", "exp1=2*x", "exp2=3*x"), + Iteration("2*x!=x*2", "exp1=2*x", "exp2=x*2"), + Iteration("x/2!=x/3", "exp1=x/2", "exp2=x/3"), + Iteration("x/2!=2/x", "exp1=x/2", "exp2=2/x"), + Iteration("x^2!=x^3", "exp1=x^2", "exp2=x^3"), + Iteration("x^2!=2^x", "exp1=x^2", "exp2=2^x"), + Iteration("x!=-2", "exp1=x", "exp2=-2"), + Iteration("x!=-x", "exp1=x", "exp2=-x"), + Iteration("sqrt(x)!=sqrt(2)", "exp1=sqrt(x)", "exp2=sqrt(2)"), + // These checks are numerically equivalent but fail due to the expression structure not + // matching. + Iteration("2==2/1", "exp1=2", "exp2=2/1"), + Iteration("1/3==0.33333333", "exp1=1/3", "exp2=0.33333333"), + Iteration("1.5==3/2", "exp1=1.5", "exp2=3/2") + ) + fun testIsApproximatelyEqualTo_bothAreSingleTermsOrOperations_butDifferent_returnsFalse() { + // Some expressions may attempt normally disallowed expressions (such as '2^x'). + val first = parseAlgebraicExpression(exp1, errorCheckingMode = REQUIRED_ONLY) + val second = parseAlgebraicExpression(exp2, errorCheckingMode = REQUIRED_ONLY) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_complexExpressionsWithNesting_allTermsMatch_returnsTrue() { + val expression = "x+2/x-(-7*8-9)+sqrt((x+2)+1)+3xy^2" + val first = parseAlgebraicExpression(expression) + val second = parseAlgebraicExpression(expression) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_complexExpressionsWithNesting_oneDifferent_returnsFalse() { + // One difference in operations, but otherwise the same values. This equality check demonstrates + // that the check is inherently recursive & properly checks for nested expression equality. + val first = parseAlgebraicExpression("x+2/x-(-7*8-9)+sqrt((x+2)+1)+3xy^2") + val second = parseAlgebraicExpression("x+2/x-(-7*8-9)+sqrt((x+1+1)+1)+3xy^2") + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_complexExpressionsWithNesting_comparedWithDefault_returnsFalse() { + val first = parseAlgebraicExpression("x+2/x-(-7*8-9)+sqrt((x+2)+1)+3xy^2") + val second = MathExpression.getDefaultInstance() + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + private companion object { private fun parseNumericExpression(expression: String): MathExpression { return parseNumericExpression(expression, ALL_ERRORS).retrieveExpectedSuccessfulResult() } - private fun parseAlgebraicExpression(expression: String): MathExpression { + private fun parseAlgebraicExpression( + expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + ): MathExpression { return parseAlgebraicExpression( - expression, allowedVariables = listOf("x", "y", "z"), ALL_ERRORS + expression, allowedVariables = listOf("x", "y", "z"), errorCheckingMode ).retrieveExpectedSuccessfulResult() } diff --git a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt index 36b1aaa5224..00da8684164 100644 --- a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt @@ -80,6 +80,14 @@ class PolynomialExtensionsTest { rational = THREE_FRACTION }.build() + private val ONE_POINT_FIVE_REAL = Real.newBuilder().apply { + irrational = 1.5 + }.build() + + private val TWO_DOUBLE_REAL = Real.newBuilder().apply { + irrational = 2.0 + }.build() + private val PI_REAL = Real.newBuilder().apply { irrational = 3.14 }.build() @@ -1250,6 +1258,806 @@ class PolynomialExtensionsTest { } } + /* Equality checks. Note that these are symmetrical to reduce the number of needed test cases. */ + + @Test + fun testIsApproximatelyEqualTo_firstIsDefault_secondIsDefault_returnsTrue() { + val first = Polynomial.getDefaultInstance() + val second = Polynomial.getDefaultInstance() + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsDefault_secondIsConstPolyOfInt2_returnsFalse() { + val first = Polynomial.getDefaultInstance() + val second = createPolynomial(createTerm(coefficient = TWO_REAL)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstPolyOfInt2_secondIsDefault_returnsFalse() { + val first = createPolynomial(createTerm(coefficient = TWO_REAL)) + val second = Polynomial.getDefaultInstance() + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfInt2_secondIsPolyOfInt2_returnsTrue() { + val first = createPolynomial(createTerm(coefficient = TWO_REAL)) + val second = createPolynomial(createTerm(coefficient = TWO_REAL)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfInt2_secondIsPolyOfInt3_returnsFalse() { + val first = createPolynomial(createTerm(coefficient = TWO_REAL)) + val second = createPolynomial(createTerm(coefficient = THREE_REAL)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfInt3_secondIsPolyOfFrac3_returnsTrue() { + val first = createPolynomial(createTerm(coefficient = THREE_REAL)) + val second = createPolynomial(createTerm(coefficient = THREE_FRACTION_REAL)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // These are equal since reals are fully evaluated for polynomials. + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfInt3_secondIsPolyOfFrac3Ones_returnsTrue() { + val first = createPolynomial(createTerm(coefficient = THREE_REAL)) + val second = createPolynomial(createTerm(coefficient = THREE_ONES_REAL)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // These are equal since reals are fully evaluated for polynomials. + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfInt3_secondIsPolyOfFracOneAndOneHalf_returnsFalse() { + val first = createPolynomial(createTerm(coefficient = THREE_REAL)) + val second = createPolynomial(createTerm(coefficient = ONE_AND_ONE_HALF_REAL)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfInt2_secondIsPolyOfDouble2_returnsTrue() { + val first = createPolynomial(createTerm(coefficient = TWO_REAL)) + val second = createPolynomial(createTerm(coefficient = TWO_DOUBLE_REAL)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // These are equal since reals are fully evaluated for polynomials. + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfInt2_secondIsPolyOfDouble2PlusMargin_returnsTrue() { + val first = createPolynomial(createTerm(coefficient = TWO_REAL)) + val second = createPolynomial( + createTerm(coefficient = Real.newBuilder().apply { + irrational = 2.00000001 + }.build()) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // These are equal since reals are fully evaluated with a margin check. + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfInt3_secondIsPolyOfDoublePi_returnsFalse() { + val first = createPolynomial(createTerm(coefficient = THREE_REAL)) + val second = createPolynomial(createTerm(coefficient = PI_REAL)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsDoubleOnePointFive_secondIsFracOneAndOneHalf_returnsTrue() { + val first = createPolynomial(createTerm(coefficient = ONE_POINT_FIVE_REAL)) + val second = createPolynomial(createTerm(coefficient = ONE_AND_ONE_HALF_REAL)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // These are equal since reals are fully evaluated for polynomials. + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsDoublePointThrees_secondIsFracOneThird_returnsTrue() { + val first = createPolynomial( + createTerm(coefficient = Real.newBuilder().apply { + irrational = 0.33333333333 + }.build()) + ) + val second = createPolynomial(createTerm(coefficient = ONE_THIRD_REAL)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // These are equal since reals are fully evaluated with a margin check. + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfVarX_secondIsPolyOfVarX_returnsTrue() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + val second = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfVarX_secondIsPolyOfVarY_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + val second = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "y", power = 1)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfVarX_secondIsPolyOfInt2_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + val second = createPolynomial(createTerm(coefficient = TWO_REAL)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfVarsXy_secondIsPolyOfVarX_returnsFalse() { + val first = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1) + ) + ) + val second = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // A variable is missing. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfVarsXy_secondIsPolyOfVarY_returnsFalse() { + val first = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1) + ) + ) + val second = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "y", power = 1)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // A variable is missing. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfVarsXy_secondIsPolyOfVarsXy_returnsTrue() { + val first = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1) + ) + ) + val second = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1) + ) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfVarsXy_secondIsPolyOfVarsYx_returnsFalse() { + val first = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1) + ) + ) + val second = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "y", power = 1), + createVariable(name = "x", power = 1) + ) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // Order matters (which is why the function recommends only comparing sorted polynomials). + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquared_secondIsXSquared_returnsTrue() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + val second = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquared_secondIsNegativeXSquared_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + val second = createPolynomial( + createTerm(coefficient = -ONE, createVariable(name = "x", power = 2)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // Coefficient sign differs. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquared_secondIsTwoXSquared_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + val second = createPolynomial( + createTerm(coefficient = TWO_REAL, createVariable(name = "x", power = 2)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // Coefficient value is different. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquared_secondIsX_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + val second = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // The powers don't match. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquared_secondIsXCubed_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + val second = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 3)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // The powers don't match. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquared_secondIsXSquaredY_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + val second = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // There's an extra variable in one of the polynomials. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquaredY_secondIsXSquaredY_returnsTrue() { + val first = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + val second = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquaredY_secondIsXy_returnsFalse() { + val first = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + val second = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1) + ) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // x's power isn't correct. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquaredY_secondIsXYSquared_returnsFalse() { + val first = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + val second = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 2) + ) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // The wrong variable is squared. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquaredY_secondIsXSquaredYSquared_returnsFalse() { + val first = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + val second = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 2) + ) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // y is incorrectly also squared. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquaredY_secondIsYXSquared_returnsFalse() { + val first = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + val second = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "y", power = 1), + createVariable(name = "x", power = 2) + ) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // The terms are out of order. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquaredY_secondIsNegativeXSquaredY_returnsFalse() { + val first = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + val second = createPolynomial( + createTerm( + coefficient = -ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // The sign is incorrect on the second polynomial. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquaredY_secondIsTwoXSquaredY_returnsFalse() { + val first = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + val second = createPolynomial( + createTerm( + coefficient = TWO_REAL, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // The coefficient is incorrect on the second polynomial. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXPlusY_secondIsXPlusY_returnsTrue() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "y", power = 1)) + ) + val second = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "y", power = 1)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXPlusY_secondIsXPlusYSquared_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "y", power = 1)) + ) + val second = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "y", power = 2)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // The second polynomial's y power doesn't match. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXPlusY_secondIsXPlusFiveY_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "y", power = 1)) + ) + val second = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = FIVE_REAL, createVariable(name = "y", power = 1)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // The second polynomial's y coefficient doesn't match. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXPlusY_secondIsNegativeXPlusY_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "y", power = 1)) + ) + val second = createPolynomial( + createTerm(coefficient = -ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "y", power = 1)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // The second polynomial's x coefficient negativity doesn't match. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_complexMultiTermMultiVarPolys_allTermsSame_returnsTrue() { + val polynomial = createPolynomial( + createTerm(coefficient = -ONE_AND_ONE_HALF_REAL, createVariable(name = "x", power = 1)), + createTerm( + coefficient = FIVE_REAL, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 3), + createVariable(name = "z", power = 1) + ), + createTerm( + coefficient = -PI_REAL, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ), + createTerm(coefficient = ONE, createVariable(name = "z", power = 1)), + createTerm(coefficient = SEVEN_REAL) + ) + + val result = polynomial.isApproximatelyEqualTo(polynomial) + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_complexMultiTermMultiVarPolys_oneItemOutOfOrder_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = -ONE_AND_ONE_HALF_REAL, createVariable(name = "x", power = 1)), + createTerm( + coefficient = FIVE_REAL, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 3), + createVariable(name = "z", power = 1) + ), + createTerm( + coefficient = -PI_REAL, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ), + createTerm(coefficient = ONE, createVariable(name = "z", power = 1)), + createTerm(coefficient = SEVEN_REAL) + ) + val second = createPolynomial( + createTerm(coefficient = -ONE_AND_ONE_HALF_REAL, createVariable(name = "x", power = 1)), + createTerm( + coefficient = FIVE_REAL, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 3), + createVariable(name = "z", power = 1) + ), + createTerm( + coefficient = -PI_REAL, + createVariable(name = "y", power = 1), + createVariable(name = "x", power = 2) + ), + createTerm(coefficient = ONE, createVariable(name = "z", power = 1)), + createTerm(coefficient = SEVEN_REAL) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // One element is out of order in the second polynomial. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_complexMultiTermMultiVarPolys_oneItemDifferent_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = -ONE_AND_ONE_HALF_REAL, createVariable(name = "x", power = 1)), + createTerm( + coefficient = FIVE_REAL, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 3), + createVariable(name = "z", power = 1) + ), + createTerm( + coefficient = -PI_REAL, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ), + createTerm(coefficient = ONE, createVariable(name = "z", power = 1)), + createTerm(coefficient = SEVEN_REAL) + ) + val second = createPolynomial( + createTerm(coefficient = -ONE_AND_ONE_HALF_REAL, createVariable(name = "x", power = 1)), + createTerm( + coefficient = FIVE_REAL - ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 3), + createVariable(name = "z", power = 1) + ), + createTerm( + coefficient = -PI_REAL, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ), + createTerm(coefficient = ONE, createVariable(name = "z", power = 1)), + createTerm(coefficient = SEVEN_REAL) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // One coefficient is different in the second polynomial. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_complexMultiTermMultiVarPolys_compareToDefault_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = -ONE_AND_ONE_HALF_REAL, createVariable(name = "x", power = 1)), + createTerm( + coefficient = FIVE_REAL, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 3), + createVariable(name = "z", power = 1) + ), + createTerm( + coefficient = -PI_REAL, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ), + createTerm(coefficient = ONE, createVariable(name = "z", power = 1)), + createTerm(coefficient = SEVEN_REAL) + ) + val second = Polynomial.getDefaultInstance() + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + /* Operator tests. */ @Test diff --git a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt index 842bd80f18c..2f8c7cd7bd9 100644 --- a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt @@ -296,6 +296,294 @@ class RealExtensionsTest { assertThat(result).isTrue() } + /* + * Approximate equality checks between reals. Note that all of these tests are symmetrical to + * reduce the number of test cases. + */ + + @Test + fun testIsApproximatelyEqualTo_firstIsDefault_secondIsInt2_throwsException() { + val first = Real.getDefaultInstance() + val second = TWO_REAL + + val exception = assertThrows(IllegalStateException::class) { + first.isApproximatelyEqualTo(second) + } + + assertThat(exception).hasMessageThat().contains("Invalid real") + } + + @Test + fun testIsApproximatelyEqualTo_firstIsInt2_secondIsDefault_throwsException() { + val first = TWO_REAL + val second = Real.getDefaultInstance() + + val exception = assertThrows(IllegalStateException::class) { + first.isApproximatelyEqualTo(second) + } + + assertThat(exception).hasMessageThat().contains("Invalid real") + } + + @Test + @RunParameterized( + Iteration("0==0", "lhsInt=0", "rhsInt=0"), + Iteration("1==1", "lhsInt=1", "rhsInt=1"), + Iteration("2==2", "lhsInt=2", "rhsInt=2"), + Iteration("-2==-2", "lhsInt=-2", "rhsInt=-2") + ) + fun testIsApproximatelyEqualTo_oneIsInt_otherIsSameInt_returnsTrue() { + val first = createIntegerReal(lhsInt) + val second = createIntegerReal(rhsInt) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // Verify both correctness and the symmetric of equality. + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + @RunParameterized( + Iteration("0!=1", "lhsInt=0", "rhsInt=1"), + Iteration("0!=2", "lhsInt=0", "rhsInt=2"), + Iteration("-2!=2", "lhsInt=-2", "rhsInt=2"), + Iteration("-2!=-1", "lhsInt=-2", "rhsInt=-1") + ) + fun testIsApproximatelyEqualTo_oneIsInt_otherIsDifferentInt_returnsFalse() { + val first = createIntegerReal(lhsInt) + val second = createIntegerReal(rhsInt) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = first.isApproximatelyEqualTo(second) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + @RunParameterized( + Iteration("0==0", "lhsInt=0", "rhsFrac=0"), + Iteration("2==2", "lhsInt=2", "rhsFrac=2"), + Iteration("2==2/1", "lhsInt=2", "rhsFrac=2/1"), + Iteration("2==4/2", "lhsInt=2", "rhsFrac=4/2"), + Iteration("-2==-2", "lhsInt=-2", "rhsFrac=-2"), + Iteration("-2==-2/1", "lhsInt=-2", "rhsFrac=-2/1"), + Iteration("-2==-4/2", "lhsInt=-2", "rhsFrac=-4/2") + ) + fun testIsApproximatelyEqualTo_oneIsInt_otherIsSameFraction_returnsTrue() { + val first = createIntegerReal(lhsInt) + val second = createRationalReal(rhsFrac) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // Verify both correctness and the symmetric of equality. + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + @RunParameterized( + Iteration("0!=2", "lhsInt=0", "rhsFrac=2"), + Iteration("2!=4", "lhsInt=2", "rhsFrac=4"), + Iteration("2!=3/2", "lhsInt=2", "rhsFrac=3/2"), + Iteration("2!=-2", "lhsInt=2", "rhsFrac=-2"), + ) + fun testIsApproximatelyEqualTo_oneIsInt_otherIsDifferentFraction_returnsFalse() { + val first = createIntegerReal(lhsInt) + val second = createRationalReal(rhsFrac) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = first.isApproximatelyEqualTo(second) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + @RunParameterized( + Iteration("0==0.0", "lhsInt=0", "rhsDouble=0.0"), + Iteration("1==1.0", "lhsInt=1", "rhsDouble=1.0"), + Iteration("2==2.0", "lhsInt=2", "rhsDouble=2.0"), + Iteration("2==2.0000001", "lhsInt=2", "rhsDouble=2.0000001"), + Iteration("2==1.9999999", "lhsInt=2", "rhsDouble=1.9999999"), + Iteration("-2==-2.0", "lhsInt=-2", "rhsDouble=-2.0"), + Iteration("-2==-2.0000001", "lhsInt=-2", "rhsDouble=-2.0000001"), + Iteration("-2==-1.9999999", "lhsInt=-2", "rhsDouble=-1.9999999") + ) + fun testIsApproximatelyEqualTo_oneIsInt_otherIsSimilarDouble_returnsTrue() { + val first = createIntegerReal(lhsInt) + val second = createIrrationalReal(rhsDouble) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // Verify both correctness and the symmetric of equality. + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + @RunParameterized( + Iteration("0!=2.0", "lhsInt=0", "rhsDouble=2.0"), + Iteration("2!=0.0", "lhsInt=2", "rhsDouble=0.0"), + Iteration("2!=4.0", "lhsInt=2", "rhsDouble=4.0"), + Iteration("3!=3.14", "lhsInt=3", "rhsDouble=3.14"), + Iteration("2!=2.001", "lhsInt=2", "rhsDouble=2.001"), + Iteration("2!=1.999", "lhsInt=2", "rhsDouble=1.999"), + Iteration("2!=-2.0", "lhsInt=2", "rhsDouble=-2.0"), + Iteration("-2!=2.0", "lhsInt=-2", "rhsDouble=2.0"), + Iteration("-2!=-2.001", "lhsInt=-2", "rhsDouble=-2.001"), + Iteration("-2!=-1.999", "lhsInt=-2", "rhsDouble=-1.999") + ) + fun testIsApproximatelyEqualTo_oneIsInt_otherIsDifferentDouble_returnsFalse() { + val first = createIntegerReal(lhsInt) + val second = createIrrationalReal(rhsDouble) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = first.isApproximatelyEqualTo(second) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + @RunParameterized( + Iteration("0==0", "lhsFrac=0", "rhsFrac=0"), + Iteration("2==2", "lhsFrac=2", "rhsFrac=2"), + Iteration("2==4/2", "lhsFrac=2", "rhsFrac=4/2"), + Iteration("3/2==1 1/2", "lhsFrac=3/2", "rhsFrac=1 1/2"), + Iteration("-2==-2", "lhsFrac=-2", "rhsFrac=-2"), + Iteration("-2==-4/2", "lhsFrac=-2", "rhsFrac=-4/2"), + Iteration("-3/2==-1 1/2", "lhsFrac=-3/2", "rhsFrac=-1 1/2"), + Iteration("1/3==3/9", "lhsFrac=1/3", "rhsFrac=3/9") + ) + fun testIsApproximatelyEqualTo_oneIsFraction_otherIsSameFraction_returnsTrue() { + val first = createRationalReal(lhsFrac) + val second = createRationalReal(rhsFrac) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // Verify both correctness and the symmetric of equality. + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + @RunParameterized( + Iteration("0!=2", "lhsFrac=0", "rhsFrac=2"), + Iteration("3/2!=1/2", "lhsFrac=3/2", "rhsFrac=1/2"), + Iteration("3/2!=1", "lhsFrac=3/2", "rhsFrac=1"), + Iteration("3/2!=-1 1/2", "lhsFrac=3/2", "rhsFrac=-1 1/2"), + Iteration("-3/2!=1 1/2", "lhsFrac=-3/2", "rhsFrac=1 1/2"), + Iteration("-3/2!=-1/2", "lhsFrac=-3/2", "rhsFrac=-1/2"), + Iteration("1/3!=2/3", "lhsFrac=1/3", "rhsFrac=2/3") + ) + fun testIsApproximatelyEqualTo_oneIsFraction_otherIsDifferentFraction_returnsFalse() { + val first = createRationalReal(lhsFrac) + val second = createRationalReal(rhsFrac) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = first.isApproximatelyEqualTo(second) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + @RunParameterized( + Iteration("0==0.0", "lhsFrac=0", "rhsDouble=0.0"), + Iteration("2==2.0", "lhsFrac=2", "rhsDouble=2.0"), + Iteration("2/1==2.0", "lhsFrac=2/1", "rhsDouble=2.0"), + Iteration("3/2==1.5", "lhsFrac=3/2", "rhsDouble=1.5"), + Iteration("1/3==0.33333333333", "lhsFrac=1/3", "rhsDouble=0.33333333333"), + Iteration("1 2/3==1.66666666666", "lhsFrac=1 2/3", "rhsDouble=1.66666666666"), + Iteration("-2==-2.0", "lhsFrac=-2", "rhsDouble=-2.0"), + Iteration("-3/2==-1.5", "lhsFrac=-3/2", "rhsDouble=-1.5") + ) + fun testIsApproximatelyEqualTo_oneIsFraction_otherIsSimilarDouble_returnsTrue() { + val first = createRationalReal(lhsFrac) + val second = createIrrationalReal(rhsDouble) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // Verify both correctness and the symmetric of equality. + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + @RunParameterized( + Iteration("0!=2.0", "lhsFrac=0", "rhsDouble=2.0"), + Iteration("2!=0.0", "lhsFrac=2", "rhsDouble=0.0"), + Iteration("2/2!=2.0", "lhsFrac=2/2", "rhsDouble=2.0"), + Iteration("1/3!=0.333", "lhsFrac=1/3", "rhsDouble=0.333"), + Iteration("1 2/3!=1.667", "lhsFrac=1 2/3", "rhsDouble=1.667"), + Iteration("22/7!=3.14", "lhsFrac=22/7", "rhsDouble=3.14"), + Iteration("-2!=2.0", "lhsFrac=-2", "rhsDouble=2.0"), + Iteration("2!=-2.0", "lhsFrac=2", "rhsDouble=-2.0"), + Iteration("-2/2!=-2.0", "lhsFrac=-2/2", "rhsDouble=-2.0"), + Iteration("-1/3!=-0.333", "lhsFrac=-1/3", "rhsDouble=-0.333"), + Iteration("-1 2/3!=-1.667", "lhsFrac=-1 2/3", "rhsDouble=-1.667") + ) + fun testIsApproximatelyEqualTo_oneIsFraction_firstIsDifferentDouble_returnsFalse() { + val first = createRationalReal(lhsFrac) + val second = createIrrationalReal(rhsDouble) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = first.isApproximatelyEqualTo(second) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + @RunParameterized( + Iteration("0.0==0.0", "lhsDouble=0.0", "rhsDouble=0.0"), + Iteration("2.0==2.0", "lhsDouble=2.0", "rhsDouble=2.0"), + Iteration("2.0000000001==1.9999999999", "lhsDouble=2.0000000001", "rhsDouble=1.9999999999"), + Iteration("3.14==3.14", "lhsDouble=3.14", "rhsDouble=3.14"), + Iteration("-2.0==-2.0", "lhsDouble=-2.0", "rhsDouble=-2.0"), + Iteration("-2.0000000001==-1.9999999999", "lhsDouble=-2.0000000001", "rhsDouble=-1.9999999999"), + Iteration("-3.14==-3.14", "lhsDouble=-3.14", "rhsDouble=-3.14") + ) + fun testIsApproximatelyEqualTo_oneIsDouble_otherIsSimilarDouble_returnsTrue() { + val first = createIrrationalReal(lhsDouble) + val second = createIrrationalReal(rhsDouble) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // Verify both correctness and the symmetric of equality. + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + @RunParameterized( + Iteration("0.0!=2.0", "lhsDouble=0.0", "rhsDouble=2.0"), + Iteration("2.001!=1.999", "lhsDouble=2.001", "rhsDouble=1.999"), + Iteration("2.7!=3.14", "lhsDouble=2.7", "rhsDouble=3.14"), + Iteration("2.7!=-3.14", "lhsDouble=2.7", "rhsDouble=-3.14"), + Iteration("-2.7!=3.14", "lhsDouble=-2.7", "rhsDouble=3.14"), + Iteration("-2.0!=2.0", "lhsDouble=-2.0", "rhsDouble=2.0"), + Iteration("-3.14!=3.14", "lhsDouble=-3.14", "rhsDouble=3.14") + ) + fun testIsApproximatelyEqualTo_oneIsDouble_otherIsDifferentDouble_returnsFalse() { + val first = createIrrationalReal(lhsDouble) + val second = createIrrationalReal(rhsDouble) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = first.isApproximatelyEqualTo(second) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + @Test fun testIsApproximatelyEqualTo_zeroAndOne_returnsFalse() { val result = ZERO_REAL.isApproximatelyEqualTo(1.0) From 4030aa34d8f73d3f027d47839c42a2269cc180c8 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 15:10:03 -0800 Subject: [PATCH 093/134] Add classifier tests. --- ...putIsEquivalentToRuleClassifierProvider.kt | 1 - ...atchesExactlyWithRuleClassifierProvider.kt | 1 - ...vialManipulationsRuleClassifierProvider.kt | 1 - .../rules/numericexpressioninput/BUILD.bazel | 12 +- ...sEquivalentToRuleClassifierProviderTest.kt | 318 ++++++++++++++++- ...esExactlyWithRuleClassifierProviderTest.kt | 323 +++++++++++++++-- ...ManipulationsRuleClassifierProviderTest.kt | 332 +++++++++++++++++- 7 files changed, 921 insertions(+), 67 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt index 9e533395999..5d041e3e2ce 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -13,7 +13,6 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingRes import org.oppia.android.util.math.evaluateAsNumericExpression import org.oppia.android.util.math.isApproximatelyEqualTo -// TODO: add tests. class NumericExpressionInputIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, private val consoleLogger: ConsoleLogger diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt index da2c5ce46c8..a62e75d9acb 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -12,7 +12,6 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingRes import javax.inject.Inject import org.oppia.android.util.math.isApproximatelyEqualTo -// TODO: add tests. class NumericExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, private val consoleLogger: ConsoleLogger diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index 34c94f3f489..b4f4b074099 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -13,7 +13,6 @@ import org.oppia.android.util.math.toComparableOperation import javax.inject.Inject import org.oppia.android.util.math.isApproximatelyEqualTo -// TODO: add tests. class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel index a83a60b85cf..f757f37b29e 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel @@ -15,10 +15,12 @@ oppia_android_test( ":dagger", "//domain", "//model/src/main/proto:interaction_object_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", "//testing/src/main/java/org/oppia/android/testing/threading:test_module", "//testing/src/main/java/org/oppia/android/testing/time:test_module", - "//third_party:androidx_test_ext_junit", + "//third_party:androidx_test_core", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", @@ -38,10 +40,12 @@ oppia_android_test( ":dagger", "//domain", "//model/src/main/proto:interaction_object_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", "//testing/src/main/java/org/oppia/android/testing/threading:test_module", "//testing/src/main/java/org/oppia/android/testing/time:test_module", - "//third_party:androidx_test_ext_junit", + "//third_party:androidx_test_core", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", @@ -61,10 +65,12 @@ oppia_android_test( ":dagger", "//domain", "//model/src/main/proto:interaction_object_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", "//testing/src/main/java/org/oppia/android/testing/threading:test_module", "//testing/src/main/java/org/oppia/android/testing/time:test_module", - "//third_party:androidx_test_ext_junit", + "//third_party:androidx_test_core", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt index 4f71926b3d4..3f9b00babce 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt @@ -3,7 +3,6 @@ package org.oppia.android.domain.classify.rules.numericexpressioninput import android.app.Application import android.content.Context import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import dagger.BindsInstance import dagger.Component @@ -17,6 +16,10 @@ import org.junit.runner.RunWith import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule @@ -28,13 +31,18 @@ import org.robolectric.annotation.LooperMode /** Tests for [NumericExpressionInputIsEquivalentToRuleClassifierProvider]. */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") -@RunWith(AndroidJUnit4::class) +@RunWith(OppiaParameterizedTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class NumericExpressionInputIsEquivalentToRuleClassifierProviderTest { + // TODO: add details about the sheet to this test's KDoc. + @Inject internal lateinit var provider: NumericExpressionInputIsEquivalentToRuleClassifierProvider + @Parameter lateinit var answer: String + @Parameter lateinit var input: String + private lateinit var classifier: RuleClassifier @Before @@ -43,33 +51,311 @@ class NumericExpressionInputIsEquivalentToRuleClassifierProviderTest { classifier = provider.createRuleClassifier() } - // TODO: finish tests. + @Test + @RunParameterized( + Iteration("0==0", "answer=0", "input=0"), + Iteration("1==1", "answer=1", "input=1"), + Iteration("2==2", "answer=2", "input=2") + ) + fun testMatches_sameSingleTerms_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } @Test - fun test() { - val answerExpression = createMathExpression("0") - val inputExpression = createMathExpression("1") + @RunParameterized( + Iteration("-2==-2", "answer=-2", "input=-2"), + Iteration("1+3.14==1+3.14", "answer=1+3.14", "input=1+3.14"), + Iteration(" 1 + 3.14 ==1+3.14", "answer= 1 + 3.14 ", "input=1+3.14"), + Iteration("1+2+3==1+2+3", "answer=1+2+3", "input=1+2+3"), + Iteration("1-3.14==1-3.14", "answer=1-3.14", "input=1-3.14"), + Iteration("2*3.14==2*3.14", "answer=2*3.14", "input=2*3.14"), + Iteration("2/3==2/3", "answer=2/3", "input=2/3"), + Iteration("2/3.14==2/3.14", "answer=2/3.14", "input=2/3.14"), + Iteration("2^3==2^3", "answer=2^3", "input=2^3"), + Iteration("2^3.14==2^3.14", "answer=2^3.14", "input=2^3.14"), + Iteration("sqrt(2)==sqrt(2)", "answer=sqrt(2)", "input=sqrt(2)") + ) + fun testMatches_sameSingleOperations_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) - val matches = - classifier.matches( - answerExpression, - inputs = mapOf("x" to inputExpression), - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() - ) + val matches = matchesClassifier(answerExpression, inputExpression) + // If the two expressions are exactly the same, the classifier should match. assertThat(matches).isTrue() } - private fun setUpTestApplicationComponent() { - DaggerNumericExpressionInputIsEquivalentToRuleClassifierProviderTest_TestApplicationComponent - .builder() - .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + @Test + @RunParameterized( + Iteration("1!=0", "answer=1", "input=0"), + Iteration("0!=1", "answer=0", "input=1"), + Iteration("3.14!=1", "answer=3.14", "input=1"), + Iteration("1!=3.14", "answer=1", "input=3.14") + ) + fun testMatches_differentSingleTerms_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions don't evaluate to the same value then the classifier won't match them. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("3.14+1==1+3.14", "answer=3.14+1", "input=1+3.14"), + Iteration("3+2+1==1+2+3", "answer=3+2+1", "input=1+2+3"), + Iteration("-3.14+1==1-3.14", "answer=-3.14+1", "input=1-3.14"), + Iteration("3.14*2==2*3.14", "answer=3.14*2", "input=2*3.14") + ) + fun testMatches_operationsDiffer_byCommutativity_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Reordering terms by commutativity is allowed by this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("1+(2+3)==(1+2)+3", "answer=1+(2+3)", "input=(1+2)+3"), + Iteration("2*(3*4)==(2*3)*4", "answer=2*(3*4)", "input=(2*3)*4") + ) + fun testMatches_operationsDiffer_byAssociativity_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Changing operation associativity is allowed by this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("3.14-1!=1-3.14", "answer=3.14-1", "input=1-3.14"), + Iteration("1-(2-3)!=(1-2)-3", "answer=1-(2-3)", "input=(1-2)-3"), + Iteration("3.14/2!=2/3.14", "answer=3.14/2", "input=2/3.14"), + Iteration("2/(3/4)!=(2/3)/4", "answer=2/(3/4)", "input=(2/3)/4"), + Iteration("3.14^2!=2^3.14", "answer=3.14^2", "input=2^3.14"), + Iteration("3.14-x!=x-3.14", "answer=3.14-x", "input=x-3.14"), + Iteration("x-(y-z)!=(x-y)-z", "answer=x-(y-z)", "input=(x-y)-z"), + Iteration("3.14/x!=x/3.14", "answer=3.14/x", "input=x/3.14"), + Iteration("x/(y/z)!=(x/y)/z", "answer=x/(y/z)", "input=(x/y)/z") + ) + fun testMatches_operationsDiffer_byNonCommutativeOrAssociativeReordering_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Non-commutative and non-associative reordering generally results in a different value, so the + // classifier will fail to match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("1-2==-(2-1)", "answer=1-2", "input=-(2-1)"), + Iteration("1+2==1+1+1", "answer=1+2", "input=1+1+1"), + Iteration("1+2==1-(-2)", "answer=1+2", "input=1-(-2)"), + Iteration("4-6==1-2-1", "answer=4-6", "input=1-2-1"), + Iteration("2*3*2*2==2*3*4", "answer=2*3*2*2", "input=2*3*4"), + Iteration("-6-2==2*-(3+1)", "answer=-6-2", "input=2*-(3+1)"), + Iteration("2/3/2/2==2/3/4", "answer=2/3/2/2", "input=2/3/4"), + Iteration("2^(2+1)==2^3", "answer=2^(2+1)", "input=2^3"), + Iteration("2^(-1)==1/2", "answer=2^(-1)", "input=1/2") + ) + fun testMatches_operationsDiffer_byDistributionAndCombining_returnTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier supports any distribution or combining of terms. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( +// Iteration("2*(2+6+3+4)==2*(2+6+3+4)", "answer=2*(2+6+3+4)", "input=2*(2+6+3+4)"), +// Iteration("2 × (2+6+3+4)==2*(2+6+3+4)", "answer=2 × (2+6+3+4)", "input=2*(2+6+3+4)"), +// Iteration( +// "15 - (6 × 2) + 3==15 - (6 × 2) + 3", "answer=15 - (6 × 2) + 3", "input=15 - (6 × 2) + 3" +// ), +// Iteration( +// "2 × (50 + 150 + 100 + 25) ==(50 + 150 + 100 + 25) × 2", +// "answer=2 × (50 + 150 + 100 + 25) ", +// "input=(50 + 150 + 100 + 25) × 2" +// ), +// Iteration( +// "2 * (50 + 150 + 100 + 25) ==2 × (50 + 150 + 100 + 25)", +// "answer=2 * (50 + 150 + 100 + 25) ", +// "input=2 × (50 + 150 + 100 + 25)" +// ), +// Iteration("2+5==5+2", "answer=2+5", "input=5+2"), +// Iteration("5+2==5+2", "answer=5+2", "input=5+2"), +// Iteration("6 − (− 4)==6 − (− 4)", "answer=6 − (− 4)", "input=6 − (− 4)"), +// Iteration("6-(-4)==6 − (− 4)", "answer=6-(-4)", "input=6 − (− 4)"), +// Iteration("− (− 4) + 6==6 − (− 4)", "answer=− (− 4) + 6", "input=6 − (− 4)"), +// Iteration("10==6 − (− 4)", "answer=10", "input=6 − (− 4)"), +// Iteration("6 + 4==6 − (− 4)", "answer=6 + 4", "input=6 − (− 4)"), +// Iteration("6 + 2^2==6 − (− 4)", "answer=6 + 2^2", "input=6 − (− 4)"), +// Iteration("3 * 2 − (− 4)==6 − (− 4)", "answer=3 * 2 − (− 4)", "input=6 − (− 4)"), +// Iteration("100/10==6 − (− 4)", "answer=100/10", "input=6 − (− 4)"), +// Iteration("3 * 10^-5==3 * 10^-5", "answer=3 * 10^-5", "input=3 * 10^-5"), +// Iteration("3/(10 * 10^4)==3 * 10^-5", "answer=3/(10 * 10^4)", "input=3 * 10^-5"), +// Iteration( +// "1000 + 200 + 30 + 4 + 0.5 + 0.06==1000 + 200 + 30 + 4 + 0.5 + 0.06", +// "answer=1000 + 200 + 30 + 4 + 0.5 + 0.06", +// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" +// ), +// Iteration( +// "200 + 30 + 4 + 0.5 + 0.06 + 1000==1000 + 200 + 30 + 4 + 0.5 + 0.06", +// "answer=200 + 30 + 4 + 0.5 + 0.06 + 1000", +// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" +// ), +// Iteration( +// "0.06 + 0.5 + 4 + 30 + 200 + 1000==1000 + 200 + 30 + 4 + 0.5 + 0.06", +// "answer=0.06 + 0.5 + 4 + 30 + 200 + 1000", +// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" +// ), +// Iteration( +// "1234.56==1000 + 200 + 30 + 4 + 0.5 + 0.06", +// "answer=1234.56", +// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" +// ), +// Iteration( +// "123456/100==1000 + 200 + 30 + 4 + 0.5 + 0.06", +// "answer=123456/100", +// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" +// ), +// Iteration( +// "61728/50==1000 + 200 + 30 + 4 + 0.5 + 0.06", +// "answer=61728/50", +// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" +// ), +// Iteration( +// "1234 + 56/100==1000 + 200 + 30 + 4 + 0.5 + 0.06", +// "answer=1234 + 56/100", +// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" +// ), +// Iteration( +// "1230 + 4.56==1000 + 200 + 30 + 4 + 0.5 + 0.06", +// "answer=1230 + 4.56", +// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" +// ), +// Iteration("2 * 2 * 3 * 3==2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3", "input=2 * 2 * 3 * 3"), +// Iteration( +// "2 * 2 * 3 * 3 * 1==2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3 * 1", "input=2 * 2 * 3 * 3" +// ), +// Iteration("2 * 2 * 9==2 * 2 * 3 * 3", "answer=2 * 2 * 9", "input=2 * 2 * 3 * 3"), +// Iteration("4 * 3^2==2 * 2 * 3 * 3", "answer=4 * 3^2", "input=2 * 2 * 3 * 3"), +// Iteration("8/2 * 3 * 3==2 * 2 * 3 * 3", "answer=8/2 * 3 * 3", "input=2 * 2 * 3 * 3"), +// Iteration("36==2 * 2 * 3 * 3", "answer=36", "input=2 * 2 * 3 * 3"), +// Iteration("sqrt(4-2)==sqrt(2)", "answer=sqrt(4-2)", "input=sqrt(2)"), +// Iteration("2*(6+3+4) + 4==2*(2+6+3+4)", "answer=2*(6+3+4) + 4", "input=2*(2+6+3+4)"), +// Iteration("2*(2+6+3) + 8==2*(2+6+3+4)", "answer=2*(2+6+3) + 8", "input=2*(2+6+3+4)"), +// Iteration("(2+6+3+4)*2==2*(2+6+3+4)", "answer=(2+6+3+4)*2", "input=2*(2+6+3+4)"), +// Iteration("(2+6+3+4) × 2==2*(2+6+3+4)", "answer=(2+6+3+4) × 2", "input=2*(2+6+3+4)"), +// Iteration("15 - 12 + 3==15 - (6 × 2) + 3", "answer=15 - 12 + 3", "input=15 - (6 × 2) + 3"), +// Iteration( +// "3 - (6 * 2) + 15==15 - (6 × 2) + 3", "answer=3 - (6 * 2) + 15", "input=15 - (6 × 2) + 3" +// ), +// Iteration( +// "15 - (2 × 6) + 3==15 - (6 × 2) + 3", "answer=15 - (2 × 6) + 3", "input=15 - (6 × 2) + 3" +// ), +// Iteration( +// "2 *(50 + 150) + 2*(100 + 25)==(50 + 150 + 100 + 25) × 2", +// "answer=2 *(50 + 150) + 2*(100 + 25)", +// "input=(50 + 150 + 100 + 25) × 2" +// ), +// Iteration( +// "2* ( 25+50+100+150)==(50 + 150 + 100 + 25) × 2", +// "answer=2* ( 25+50+100+150)", +// "input=(50 + 150 + 100 + 25) × 2" +// ), +// Iteration("10^−5 * 3==3 * 10^-5", "answer=10^−5 * 3", "input=3 * 10^-5"), +// Iteration("30 * 10^−6==3 * 10^-5", "answer=30 * 10^−6", "input=3 * 10^-5"), +// Iteration("0.00003==3 * 10^-5", "answer=0.00003", "input=3 * 10^-5"), +// Iteration("3/10^5==3 * 10^-5", "answer=3/10^5", "input=3 * 10^-5"), + Iteration("3 *2 – (− 4)==6 − (− 4)", "answer=3 *2 – (− 4)", "input=6 − (− 4)"), +// Iteration("7==5+2", "answer=7", "input=5+2"), +// Iteration("3+4==5+2", "answer=3+4", "input=5+2") + ) + fun testMatches_assortedExpressions_withMatchingCharacteristics_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // pass for this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("3 * 10^5!=3 * 10^-5", "answer=3 * 10^5", "input=3 * 10^-5"), + Iteration("2 * 10^−5!=3 * 10^-5", "answer=2 * 10^−5", "input=3 * 10^-5"), + Iteration("5 * 10^−3!=3 * 10^-5", "answer=5 * 10^−3", "input=3 * 10^-5"), + Iteration( + "123456!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=123456", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1000 + 200 + 30!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1000 + 200 + 30", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration("6 − 4!=6 − (− 4)", "answer=6 − 4", "input=6 − (− 4)"), + Iteration("6 + (− 4)!=6 − (− 4)", "answer=6 + (− 4)", "input=6 − (− 4)"), + Iteration("100!=6 − (− 4)", "answer=100", "input=6 − (− 4)"), + Iteration("2 * 2 * 3!=2 * 2 * 3 * 3", "answer=2 * 2 * 3", "input=2 * 2 * 3 * 3"), + Iteration("2 * 3 * 3 * 3!=2 * 2 * 3 * 3", "answer=2 * 3 * 3 * 3", "input=2 * 2 * 3 * 3") + ) + fun testMatches_assortedExpressions_withoutMatchingCharacteristics_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // not pass for this classifier. + assertThat(matches).isFalse() + } + + private fun matchesClassifier( + answerExpression: InteractionObject, + inputExpression: InteractionObject + ): Boolean { + return classifier.matches( + answerExpression, + inputs = mapOf("x" to inputExpression), + writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + ) } private fun createMathExpression(rawExpression: String) = InteractionObject.newBuilder().apply { mathExpression = rawExpression }.build() + private fun setUpTestApplicationComponent() { + DaggerNumericExpressionInputIsEquivalentToRuleClassifierProviderTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + // TODO(#89): Move this to a common test application component. @Module class TestModule { diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt index f69864d89b6..42b4c7e2b99 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt @@ -3,7 +3,6 @@ package org.oppia.android.domain.classify.rules.numericexpressioninput import android.app.Application import android.content.Context import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import dagger.BindsInstance import dagger.Component @@ -17,6 +16,10 @@ import org.junit.runner.RunWith import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule @@ -28,13 +31,18 @@ import org.robolectric.annotation.LooperMode /** Tests for [NumericExpressionInputMatchesExactlyWithRuleClassifierProvider]. */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") -@RunWith(AndroidJUnit4::class) +@RunWith(OppiaParameterizedTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest { + // TODO: add details about the sheet to this test's KDoc. + @Inject internal lateinit var provider: NumericExpressionInputMatchesExactlyWithRuleClassifierProvider + @Parameter lateinit var answer: String + @Parameter lateinit var input: String + private lateinit var classifier: RuleClassifier @Before @@ -43,46 +51,303 @@ class NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest { classifier = provider.createRuleClassifier() } - // TODO: finish tests. - - // testMatches_zeroAnswer_zeroInput_returnsTrue - // testMatches_zeroAnswer_oneInput_returnsFalse - // testMatches_oneAnswer_zeroInput_returnsFalse - // testMatches_oneAnswer_oneInput_returnsTrue - // testMatches_answerAndInput_sameOperationParameters_returnsTrue - // testMatches_answerAndInput_same_returnsTrue - // testMatches_answerAndInput_differentByCommutativity_returnsFalse - // testMatches_answerAndInput_differentByAssociativity_returnsFalse - // testMatches_answerAndInput_differentByDistributivity_returnsFalse - // testMatches_answerAndInput_differentByNonCommutativeReordering_returnsFalse - // testMatches_answerAndInput_similarInMultipleWays_returnsTrue - // testMatches_answerAndInput_varyIncorrectlyInMultipleWays_returnsFalse + @Test + @RunParameterized( + Iteration("0==0", "answer=0", "input=0"), + Iteration("1==1", "answer=1", "input=1"), + Iteration("2==2", "answer=2", "input=2") + ) + fun testMatches_sameSingleTerms_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } @Test - fun test() { - val answerExpression = createMathExpression("0") - val inputExpression = createMathExpression("1") + @RunParameterized( + Iteration("-2==-2", "answer=-2", "input=-2"), + Iteration("1+3.14==1+3.14", "answer=1+3.14", "input=1+3.14"), + Iteration(" 1 + 3.14 ==1+3.14", "answer= 1 + 3.14 ", "input=1+3.14"), + Iteration("1+2+3==1+2+3", "answer=1+2+3", "input=1+2+3"), + Iteration("1-3.14==1-3.14", "answer=1-3.14", "input=1-3.14"), + Iteration("2*3.14==2*3.14", "answer=2*3.14", "input=2*3.14"), + Iteration("2/3==2/3", "answer=2/3", "input=2/3"), + Iteration("2/3.14==2/3.14", "answer=2/3.14", "input=2/3.14"), + Iteration("2^3==2^3", "answer=2^3", "input=2^3"), + Iteration("2^3.14==2^3.14", "answer=2^3.14", "input=2^3.14"), + Iteration("sqrt(2)==sqrt(2)", "answer=sqrt(2)", "input=sqrt(2)") + ) + fun testMatches_sameSingleOperations_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) - val matches = - classifier.matches( - answerExpression, - inputs = mapOf("x" to inputExpression), - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() - ) + val matches = matchesClassifier(answerExpression, inputExpression) + // If the two expressions are exactly the same, the classifier should match. assertThat(matches).isTrue() } - private fun setUpTestApplicationComponent() { - DaggerNumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest_TestApplicationComponent - .builder() - .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + @Test + @RunParameterized( + Iteration("1!=0", "answer=1", "input=0"), + Iteration("0!=1", "answer=0", "input=1"), + Iteration("3.14!=1", "answer=3.14", "input=1"), + Iteration("1!=3.14", "answer=1", "input=3.14") + ) + fun testMatches_differentSingleTerms_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions aren't exactly the same (minus whitespace), they won't match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("3.14+1!=1+3.14", "answer=3.14+1", "input=1+3.14"), + Iteration("3+2+1!=1+2+3", "answer=3+2+1", "input=1+2+3"), + Iteration("-3.14+1!=1-3.14", "answer=-3.14+1", "input=1-3.14"), + Iteration("3.14*2!=2*3.14", "answer=3.14*2", "input=2*3.14") + ) + fun testMatches_operationsDiffer_byCommutativity_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier expects commutativity to be retained. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("1+(2+3)!=(1+2)+3", "answer=1+(2+3)", "input=(1+2)+3"), + Iteration("2*(3*4)!=(2*3)*4", "answer=2*(3*4)", "input=(2*3)*4") + ) + fun testMatches_operationsDiffer_byAssociativity_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier expects associativity to be retained. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("3.14-1!=1-3.14", "answer=3.14-1", "input=1-3.14"), + Iteration("1-(2-3)!=(1-2)-3", "answer=1-(2-3)", "input=(1-2)-3"), + Iteration("3.14/2!=2/3.14", "answer=3.14/2", "input=2/3.14"), + Iteration("2/(3/4)!=(2/3)/4", "answer=2/(3/4)", "input=(2/3)/4"), + Iteration("3.14^2!=2^3.14", "answer=3.14^2", "input=2^3.14"), + Iteration("3.14-x!=x-3.14", "answer=3.14-x", "input=x-3.14"), + Iteration("x-(y-z)!=(x-y)-z", "answer=x-(y-z)", "input=(x-y)-z"), + Iteration("3.14/x!=x/3.14", "answer=3.14/x", "input=x/3.14"), + Iteration("x/(y/z)!=(x/y)/z", "answer=x/(y/z)", "input=(x/y)/z") + ) + fun testMatches_operationsDiffer_byNonCommutativeOrAssociativeReordering_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Non-commutative and non-associative reordering generally results in a different value, so the + // classifier will fail to match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("1-2!=-(2-1)", "answer=1-2", "input=-(2-1)"), + Iteration("1+2!=1+1+1", "answer=1+2", "input=1+1+1"), + Iteration("1+2!=1-(-2)", "answer=1+2", "input=1-(-2)"), + Iteration("4-6!=1-2-1", "answer=4-6", "input=1-2-1"), + Iteration("2*3*2*2!=2*3*4", "answer=2*3*2*2", "input=2*3*4"), + Iteration("-6-2!=2*-(3+1)", "answer=-6-2", "input=2*-(3+1)"), + Iteration("2/3/2/2!=2/3/4", "answer=2/3/2/2", "input=2/3/4"), + Iteration("2^(2+1)!=2^3", "answer=2^(2+1)", "input=2^3"), + Iteration("2^(-1)!=1/2", "answer=2^(-1)", "input=1/2") + ) + fun testMatches_operationsDiffer_byDistributionAndCombining_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier doesn't support distributing or combining terms. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("2*(2+6+3+4)==2*(2+6+3+4)", "answer=2*(2+6+3+4)", "input=2*(2+6+3+4)"), + Iteration("2 × (2+6+3+4)==2*(2+6+3+4)", "answer=2 × (2+6+3+4)", "input=2*(2+6+3+4)"), + Iteration( + "15 - (6 × 2) + 3==15 - (6 × 2) + 3", "answer=15 - (6 × 2) + 3", "input=15 - (6 × 2) + 3" + ), + Iteration( + "2 * (50 + 150 + 100 + 25) ==2 × (50 + 150 + 100 + 25)", + "answer=2 * (50 + 150 + 100 + 25) ", + "input=2 × (50 + 150 + 100 + 25)" + ), + Iteration("5+2==5+2", "answer=5+2", "input=5+2"), + Iteration("6 − (− 4)==6 − (− 4)", "answer=6 − (− 4)", "input=6 − (− 4)"), + Iteration("6-(-4)==6 − (− 4)", "answer=6-(-4)", "input=6 − (− 4)"), + Iteration("3 * 10^-5==3 * 10^-5", "answer=3 * 10^-5", "input=3 * 10^-5"), + Iteration( + "1000 + 200 + 30 + 4 + 0.5 + 0.06==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration("2 * 2 * 3 * 3==2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3", "input=2 * 2 * 3 * 3") + ) + fun testMatches_assortedExpressions_withMatchingCharacteristics_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // pass for this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration( + "2 × (50 + 150 + 100 + 25) !=(50 + 150 + 100 + 25) × 2", + "answer=2 × (50 + 150 + 100 + 25) ", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration("− (− 4) + 6!=6 − (− 4)", "answer=− (− 4) + 6", "input=6 − (− 4)"), + Iteration("10!=6 − (− 4)", "answer=10", "input=6 − (− 4)"), + Iteration("6 + 4!=6 − (− 4)", "answer=6 + 4", "input=6 − (− 4)"), + Iteration("6 + 2^2!=6 − (− 4)", "answer=6 + 2^2", "input=6 − (− 4)"), + Iteration("3 * 2 − (− 4)!=6 − (− 4)", "answer=3 * 2 − (− 4)", "input=6 − (− 4)"), + Iteration("100/10!=6 − (− 4)", "answer=100/10", "input=6 − (− 4)"), + Iteration("2+5!=5+2", "answer=2+5", "input=5+2"), + Iteration("3/(10 * 10^4)!=3 * 10^-5", "answer=3/(10 * 10^4)", "input=3 * 10^-5"), + Iteration( + "200 + 30 + 4 + 0.5 + 0.06 + 1000!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=200 + 30 + 4 + 0.5 + 0.06 + 1000", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "0.06 + 0.5 + 4 + 30 + 200 + 1000!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=0.06 + 0.5 + 4 + 30 + 200 + 1000", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + Iteration( + "1234.56!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1234.56", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + Iteration( + "123456/100!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=123456/100", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + Iteration( + "61728/50!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=61728/50", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + Iteration( + "1234 + 56/10!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1234 + 56/10", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + Iteration( + "1230 + 4.56!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1230 + 4.56", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + Iteration( + "2 * 2 * 3 * 3 * 1!=2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3 * 1", "input=2 * 2 * 3 * 3" + ), + Iteration("2 * 2 * 9!=2 * 2 * 3 * 3", "answer=2 * 2 * 9", "input=2 * 2 * 3 * 3"), + Iteration("4 * 3^2!=2 * 2 * 3 * 3", "answer=4 * 3^2", "input=2 * 2 * 3 * 3"), + Iteration("8/2 * 3 * 3!=2 * 2 * 3 * 3", "answer=8/2 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration("36!=2 * 2 * 3 * 3", "answer=36", "input=2 * 2 * 3 * 3"), + Iteration("sqrt(4-2)!=sqrt(2)", "answer=sqrt(4-2)", "input=sqrt(2)"), + Iteration("2*(6+3+4) + 4!=2*(2+6+3+4)", "answer=2*(6+3+4) + 4", "input=2*(2+6+3+4)"), + Iteration("2*(2+6+3) + 8!=2*(2+6+3+4)", "answer=2*(2+6+3) + 8", "input=2*(2+6+3+4)"), + Iteration("(2+6+3+4)*2!=2*(2+6+3+4)", "answer=(2+6+3+4)*2", "input=2*(2+6+3+4)"), + Iteration("(2+6+3+4) × 2!=2*(2+6+3+4)", "answer=(2+6+3+4) × 2", "input=2*(2+6+3+4)"), + Iteration("15 - 12 + 3!=15 - (6 × 2) + 3", "answer=15 - 12 + 3", "input=15 - (6 × 2) + 3"), + Iteration( + "3 - (6 * 2) + 15!=15 - (6 × 2) + 3", "answer=3 - (6 * 2) + 15", "input=15 - (6 × 2) + 3" + ), + Iteration( + "15 - (2 × 6) + 3!=15 - (6 × 2) + 3", "answer=15 - (2 × 6) + 3", "input=15 - (6 × 2) + 3" + ), + Iteration( + "2 *(50 + 150) + 2*(100 + 25)!=(50 + 150 + 100 + 25) × 2", + "answer=2 *(50 + 150) + 2*(100 + 25)", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration( + "2* ( 25+50+100+150)!=(50 + 150 + 100 + 25) × 2", + "answer=2* ( 25+50+100+150)", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration("10^−5 * 3!=3 * 10^-5", "answer=10^−5 * 3", "input=3 * 10^-5"), + Iteration("3 * 10^5!=3 * 10^-5", "answer=3 * 10^5", "input=3 * 10^-5"), + Iteration("2 * 10^−5!=3 * 10^-5", "answer=2 * 10^−5", "input=3 * 10^-5"), + Iteration("5 * 10^−3!=3 * 10^-5", "answer=5 * 10^−3", "input=3 * 10^-5"), + Iteration("30 * 10^−6!=3 * 10^-5", "answer=30 * 10^−6", "input=3 * 10^-5"), + Iteration("0.00003!=3 * 10^-5", "answer=0.00003", "input=3 * 10^-5"), + Iteration("3/10^5!=3 * 10^-5", "answer=3/10^5", "input=3 * 10^-5"), + Iteration( + "123456!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=123456", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + Iteration( + "1000 + 200 + 30!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1000 + 200 + 30", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + Iteration("3 *2 – (− 4)!=6 − (− 4)", "answer=3 *2 – (− 4)", "input=6 − (− 4)"), + Iteration("6 − 4!=6 − (− 4)", "answer=6 − 4", "input=6 − (− 4)"), + Iteration("6 + (− 4)!=6 − (− 4)", "answer=6 + (− 4)", "input=6 − (− 4)"), + Iteration("100!=6 − (− 4)", "answer=100", "input=6 − (− 4)"), + Iteration("7!=5+2", "answer=7", "input=5+2"), + Iteration("3+4!=5+2", "answer=3+4", "input=5+2"), + Iteration("2 * 2 * 3!=2 * 2 * 3 * 3", "answer=2 * 2 * 3", "input=2 * 2 * 3 * 3"), + Iteration("2 * 3 * 3 * 3!=2 * 2 * 3 * 3", "answer=2 * 3 * 3 * 3", "input=2 * 2 * 3 * 3") + ) + fun testMatches_assortedExpressions_withoutMatchingCharacteristics_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // not pass for this classifier. + assertThat(matches).isFalse() + } + + private fun matchesClassifier( + answerExpression: InteractionObject, + inputExpression: InteractionObject + ): Boolean { + return classifier.matches( + answerExpression, + inputs = mapOf("x" to inputExpression), + writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + ) } private fun createMathExpression(rawExpression: String) = InteractionObject.newBuilder().apply { mathExpression = rawExpression }.build() + private fun setUpTestApplicationComponent() { + DaggerNumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + // TODO(#89): Move this to a common test application component. @Module class TestModule { diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt index 508a6243b80..bbd4085d420 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt @@ -3,7 +3,6 @@ package org.oppia.android.domain.classify.rules.numericexpressioninput import android.app.Application import android.content.Context import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import dagger.BindsInstance import dagger.Component @@ -17,6 +16,10 @@ import org.junit.runner.RunWith import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule @@ -28,13 +31,18 @@ import org.robolectric.annotation.LooperMode /** Tests for [NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider]. */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") -@RunWith(AndroidJUnit4::class) +@RunWith(OppiaParameterizedTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest { + // TODO: add details about the sheet to this test's KDoc. + @Inject internal lateinit var provider: NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider + @Parameter lateinit var answer: String + @Parameter lateinit var input: String + private lateinit var classifier: RuleClassifier @Before @@ -43,33 +51,325 @@ class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvide classifier = provider.createRuleClassifier() } - // TODO: finish tests. + @Test + @RunParameterized( + Iteration("0==0", "answer=0", "input=0"), + Iteration("1==1", "answer=1", "input=1"), + Iteration("2==2", "answer=2", "input=2") + ) + fun testMatches_sameSingleTerms_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } @Test - fun test() { - val answerExpression = createMathExpression("0") - val inputExpression = createMathExpression("1") + @RunParameterized( + Iteration("-2==-2", "answer=-2", "input=-2"), + Iteration("1+3.14==1+3.14", "answer=1+3.14", "input=1+3.14"), + Iteration(" 1 + 3.14 ==1+3.14", "answer= 1 + 3.14 ", "input=1+3.14"), + Iteration("1+2+3==1+2+3", "answer=1+2+3", "input=1+2+3"), + Iteration("1-3.14==1-3.14", "answer=1-3.14", "input=1-3.14"), + Iteration("2*3.14==2*3.14", "answer=2*3.14", "input=2*3.14"), + Iteration("2/3==2/3", "answer=2/3", "input=2/3"), + Iteration("2/3.14==2/3.14", "answer=2/3.14", "input=2/3.14"), + Iteration("2^3==2^3", "answer=2^3", "input=2^3"), + Iteration("2^3.14==2^3.14", "answer=2^3.14", "input=2^3.14"), + Iteration("sqrt(2)==sqrt(2)", "answer=sqrt(2)", "input=sqrt(2)") + ) + fun testMatches_sameSingleOperations_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) - val matches = - classifier.matches( - answerExpression, - inputs = mapOf("x" to inputExpression), - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() - ) + val matches = matchesClassifier(answerExpression, inputExpression) + // If the two expressions are exactly the same, the classifier should match. assertThat(matches).isTrue() } - private fun setUpTestApplicationComponent() { - DaggerNumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent - .builder() - .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + @Test + @RunParameterized( + Iteration("1!=0", "answer=1", "input=0"), + Iteration("0!=1", "answer=0", "input=1"), + Iteration("3.14!=1", "answer=3.14", "input=1"), + Iteration("1!=3.14", "answer=1", "input=3.14") + ) + fun testMatches_differentSingleTerms_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions aren't exactly the same (minus whitespace and some minor term + // reordering), they won't match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("3.14+1==1+3.14", "answer=3.14+1", "input=1+3.14"), + Iteration("3+2+1==1+2+3", "answer=3+2+1", "input=1+2+3"), + Iteration("-3.14+1==1-3.14", "answer=-3.14+1", "input=1-3.14"), + Iteration("3.14*2==2*3.14", "answer=3.14*2", "input=2*3.14") + ) + fun testMatches_operationsDiffer_byCommutativity_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Reordering terms by commutativity is allowed by this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("1+(2+3)==(1+2)+3", "answer=1+(2+3)", "input=(1+2)+3"), + Iteration("2*(3*4)==(2*3)*4", "answer=2*(3*4)", "input=(2*3)*4") + ) + fun testMatches_operationsDiffer_byAssociativity_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Changing operation associativity is allowed by this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("3.14-1!=1-3.14", "answer=3.14-1", "input=1-3.14"), + Iteration("1-(2-3)!=(1-2)-3", "answer=1-(2-3)", "input=(1-2)-3"), + Iteration("3.14/2!=2/3.14", "answer=3.14/2", "input=2/3.14"), + Iteration("2/(3/4)!=(2/3)/4", "answer=2/(3/4)", "input=(2/3)/4"), + Iteration("3.14^2!=2^3.14", "answer=3.14^2", "input=2^3.14"), + Iteration("3.14-x!=x-3.14", "answer=3.14-x", "input=x-3.14"), + Iteration("x-(y-z)!=(x-y)-z", "answer=x-(y-z)", "input=(x-y)-z"), + Iteration("3.14/x!=x/3.14", "answer=3.14/x", "input=x/3.14"), + Iteration("x/(y/z)!=(x/y)/z", "answer=x/(y/z)", "input=(x/y)/z") + ) + fun testMatches_operationsDiffer_byNonCommutativeOrAssociativeReordering_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Non-commutative and non-associative reordering generally results in a different value, so the + // classifier will fail to match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("1+2==1-(-2)", "answer=1+2", "input=1-(-2)") + ) + fun testMatches_operationsDiffer_byDistributingNegation_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // The classifier does support distributing negations (e.g. across groups). + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("1-2==-(2-1)", "answer=1-2", "input=-(2-1)"), + Iteration("1+2!=1+1+1", "answer=1+2", "input=1+1+1"), + Iteration("4-6!=1-2-1", "answer=4-6", "input=1-2-1"), + Iteration("2*3*2*2!=2*3*4", "answer=2*3*2*2", "input=2*3*4"), + Iteration("-6-2!=2*-(3+1)", "answer=-6-2", "input=2*-(3+1)"), + Iteration("2/3/2/2!=2/3/4", "answer=2/3/2/2", "input=2/3/4"), + Iteration("2^(2+1)!=2^3", "answer=2^(2+1)", "input=2^3"), + Iteration("2^(-1)!=1/2", "answer=2^(-1)", "input=1/2") + ) + fun testMatches_operationsDiffer_byDistributionAndCombining_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier doesn't support broadly distributing or combining terms. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("2*(2+6+3+4)==2*(2+6+3+4)", "answer=2*(2+6+3+4)", "input=2*(2+6+3+4)"), + Iteration("2 × (2+6+3+4)==2*(2+6+3+4)", "answer=2 × (2+6+3+4)", "input=2*(2+6+3+4)"), + Iteration( + "15 - (6 × 2) + 3==15 - (6 × 2) + 3", "answer=15 - (6 × 2) + 3", "input=15 - (6 × 2) + 3" + ), + Iteration( + "2 × (50 + 150 + 100 + 25) ==(50 + 150 + 100 + 25) × 2", + "answer=2 × (50 + 150 + 100 + 25) ", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration( + "2 * (50 + 150 + 100 + 25) ==2 × (50 + 150 + 100 + 25)", + "answer=2 * (50 + 150 + 100 + 25) ", + "input=2 × (50 + 150 + 100 + 25)" + ), + Iteration("2+5==5+2", "answer=2+5", "input=5+2"), + Iteration("5+2==5+2", "answer=5+2", "input=5+2"), + Iteration("6 − (− 4)==6 − (− 4)", "answer=6 − (− 4)", "input=6 − (− 4)"), + Iteration("6-(-4)==6 − (− 4)", "answer=6-(-4)", "input=6 − (− 4)"), + Iteration("− (− 4) + 6==6 − (− 4)", "answer=− (− 4) + 6", "input=6 − (− 4)"), + Iteration("6 + 4!=6 − (− 4)", "answer=6 + 4", "input=6 − (− 4)"), + Iteration("3 * 10^-5==3 * 10^-5", "answer=3 * 10^-5", "input=3 * 10^-5"), + Iteration("10^−5 * 3==3 * 10^-5", "answer=10^−5 * 3", "input=3 * 10^-5"), + Iteration( + "1000 + 200 + 30 + 4 + 0.5 + 0.06==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "200 + 30 + 4 + 0.5 + 0.06 + 1000==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=200 + 30 + 4 + 0.5 + 0.06 + 1000", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "0.06 + 0.5 + 4 + 30 + 200 + 1000==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=0.06 + 0.5 + 4 + 30 + 200 + 1000", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration("2 * 2 * 3 * 3==2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration("(2+6+3+4)*2==2*(2+6+3+4)", "answer=(2+6+3+4)*2", "input=2*(2+6+3+4)"), + Iteration("(2+6+3+4) × 2==2*(2+6+3+4)", "answer=(2+6+3+4) × 2", "input=2*(2+6+3+4)"), + Iteration( + "3 - (6 * 2) + 15==15 - (6 × 2) + 3", "answer=3 - (6 * 2) + 15", "input=15 - (6 × 2) + 3" + ), + Iteration( + "15 - (2 × 6) + 3==15 - (6 × 2) + 3", "answer=15 - (2 × 6) + 3", "input=15 - (6 × 2) + 3" + ), + Iteration( + "2* ( 25+50+100+150)==(50 + 150 + 100 + 25) × 2", + "answer=2* ( 25+50+100+150)", + "input=(50 + 150 + 100 + 25) × 2" + ) + ) + fun testMatches_assortedExpressions_withMatchingCharacteristics_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // pass for this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("10!=6 − (− 4)", "answer=10", "input=6 − (− 4)"), + Iteration("6 + 2^2!=6 − (− 4)", "answer=6 + 2^2", "input=6 − (− 4)"), + Iteration("3 * 2 − (− 4)!=6 − (− 4)", "answer=3 * 2 − (− 4)", "input=6 − (− 4)"), + Iteration("100/10!=6 − (− 4)", "answer=100/10", "input=6 − (− 4)"), + Iteration("3/(10 * 10^4)!=3 * 10^-5", "answer=3/(10 * 10^4)", "input=3 * 10^-5"), + Iteration( + "1234.56!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1234.56", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "123456/100!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=123456/100", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "61728/50!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=61728/50", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1234 + 56/10!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1234 + 56/10", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1230 + 4.56!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1230 + 4.56", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "2 * 2 * 3 * 3 * 1!=2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3 * 1", "input=2 * 2 * 3 * 3" + ), + Iteration("2 * 2 * 9!=2 * 2 * 3 * 3", "answer=2 * 2 * 9", "input=2 * 2 * 3 * 3"), + Iteration("4 * 3^2!=2 * 2 * 3 * 3", "answer=4 * 3^2", "input=2 * 2 * 3 * 3"), + Iteration("8/2 * 3 * 3!=2 * 2 * 3 * 3", "answer=8/2 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration("36!=2 * 2 * 3 * 3", "answer=36", "input=2 * 2 * 3 * 3"), + Iteration("sqrt(4-2)!=sqrt(2)", "answer=sqrt(4-2)", "input=sqrt(2)"), + Iteration("3 * 10^5!=3 * 10^-5", "answer=3 * 10^5", "input=3 * 10^-5"), + Iteration("2 * 10^−5!=3 * 10^-5", "answer=2 * 10^−5", "input=3 * 10^-5"), + Iteration("5 * 10^−3!=3 * 10^-5", "answer=5 * 10^−3", "input=3 * 10^-5"), + Iteration("30 * 10^−6!=3 * 10^-5", "answer=30 * 10^−6", "input=3 * 10^-5"), + Iteration("0.00003!=3 * 10^-5", "answer=0.00003", "input=3 * 10^-5"), + Iteration("3/10^5!=3 * 10^-5", "answer=3/10^5", "input=3 * 10^-5"), + Iteration( + "123456!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=123456", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1000 + 200 + 30!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1000 + 200 + 30", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration("3 *2 – (− 4)!=6 − (− 4)", "answer=3 *2 – (− 4)", "input=6 − (− 4)"), + Iteration("6 − 4!=6 − (− 4)", "answer=6 − 4", "input=6 − (− 4)"), + Iteration("6 + (− 4)!=6 − (− 4)", "answer=6 + (− 4)", "input=6 − (− 4)"), + Iteration("100!=6 − (− 4)", "answer=100", "input=6 − (− 4)"), + Iteration("7!=5+2", "answer=7", "input=5+2"), + Iteration("3+4!=5+2", "answer=3+4", "input=5+2"), + Iteration("2 * 2 * 3!=2 * 2 * 3 * 3", "answer=2 * 2 * 3", "input=2 * 2 * 3 * 3"), + Iteration("2 * 3 * 3 * 3!=2 * 2 * 3 * 3", "answer=2 * 3 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration( + "2 *(50 + 150) + 2*(100 + 25)!=(50 + 150 + 100 + 25) × 2", + "answer=2 *(50 + 150) + 2*(100 + 25)", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration("15 - 12 + 3!=15 - (6 × 2) + 3", "answer=15 - 12 + 3", "input=15 - (6 × 2) + 3"), + Iteration("2*(6+3+4) + 4!=2*(2+6+3+4)", "answer=2*(6+3+4) + 4", "input=2*(2+6+3+4)"), + Iteration("2*(2+6+3) + 8!=2*(2+6+3+4)", "answer=2*(2+6+3) + 8", "input=2*(2+6+3+4)") + ) + fun testMatches_assortedExpressions_withoutMatchingCharacteristics_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // not pass for this classifier. + assertThat(matches).isFalse() + } + + private fun matchesClassifier( + answerExpression: InteractionObject, + inputExpression: InteractionObject + ): Boolean { + return classifier.matches( + answerExpression, + inputs = mapOf("x" to inputExpression), + writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + ) } private fun createMathExpression(rawExpression: String) = InteractionObject.newBuilder().apply { mathExpression = rawExpression }.build() + private fun setUpTestApplicationComponent() { + DaggerNumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + // TODO(#89): Move this to a common test application component. @Module class TestModule { From d20256ce61cdf6807e9ab47f70430b830c256cfa Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 15:29:49 -0800 Subject: [PATCH 094/134] Use more intentional epsilons for float comparing. --- ...icInputEqualsRuleClassifierProviderTest.kt | 10 +++---- .../android/util/math/FloatExtensions.kt | 26 ++++++++++++++----- .../android/util/math/FloatExtensionsTest.kt | 10 +++---- .../android/util/math/RealExtensionsTest.kt | 4 +-- 4 files changed, 32 insertions(+), 18 deletions(-) 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 f7485b13545..506df4acebe 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 @@ -12,7 +12,7 @@ import org.junit.runner.RunWith import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows -import org.oppia.android.util.math.FLOAT_EQUALITY_INTERVAL +import org.oppia.android.util.math.FLOAT_EQUALITY_EPSILON import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject @@ -33,13 +33,13 @@ class NumericInputEqualsRuleClassifierProviderTest { private val NEGATIVE_REAL_VALUE_3_5 = InteractionObjectTestBuilder.createReal(value = -3.5) private val FIVE_TIMES_FLOAT_EQUALITY_INTERVAL = - InteractionObjectTestBuilder.createReal(value = 5 * FLOAT_EQUALITY_INTERVAL) + InteractionObjectTestBuilder.createReal(value = 5 * FLOAT_EQUALITY_EPSILON) private val SIX_TIMES_FLOAT_EQUALITY_INTERVAL = - InteractionObjectTestBuilder.createReal(value = 6 * FLOAT_EQUALITY_INTERVAL) + InteractionObjectTestBuilder.createReal(value = 6 * FLOAT_EQUALITY_EPSILON) private val FIVE_POINT_ONE_TIMES_FLOAT_EQUALITY_INTERVAL = InteractionObjectTestBuilder.createReal( - value = 5 * FLOAT_EQUALITY_INTERVAL + - FLOAT_EQUALITY_INTERVAL / 10 + value = 5 * FLOAT_EQUALITY_EPSILON + + FLOAT_EQUALITY_EPSILON / 10 ) private val STRING_VALUE = InteractionObjectTestBuilder.createString(value = "test") diff --git a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt index 27ac002c08c..9062dfe6484 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt @@ -2,22 +2,36 @@ package org.oppia.android.util.math import kotlin.math.abs -/** The error margin used for approximately [Float] and [Double] equality checking. */ -const val FLOAT_EQUALITY_INTERVAL = 1e-5 +/** + * The error margin used for approximately [Float] equality checking. + * + * Note that the machine epsilon value from https://en.wikipedia.org/wiki/Machine_epsilon is defined + * defined as the smallest value that, when added to, or subtract from, 1, will result in a value + * that is exactly equal to 1. A slightly larger value is picked here for some allowance in + * variance. + */ +const val FLOAT_EQUALITY_EPSILON: Float = 1e-6f + +/** + * The error margin used for approximately [Double] equality checking. + * + * See [FLOAT_EQUALITY_EPSILON] for an explanation of this value. + */ +const val DOUBLE_EQUALITY_EPSILON: Double = 1e-15 /** * Returns whether this float approximately equals another based on a consistent epsilon value - * ([FLOAT_EQUALITY_INTERVAL]). + * ([FLOAT_EQUALITY_EPSILON]). */ fun Float.approximatelyEquals(other: Float): Boolean { - return abs(this - other) < FLOAT_EQUALITY_INTERVAL + return abs(this - other) < FLOAT_EQUALITY_EPSILON } /** Returns whether this double approximately equals another based on a consistent epsilon value - * ([FLOAT_EQUALITY_INTERVAL]). + * ([DOUBLE_EQUALITY_EPSILON]). */ fun Double.approximatelyEquals(other: Double): Boolean { - return abs(this - other) < FLOAT_EQUALITY_INTERVAL + return abs(this - other) < DOUBLE_EQUALITY_EPSILON } /** diff --git a/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt index 6e1896902e6..9f83b544a6d 100644 --- a/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt @@ -36,7 +36,7 @@ class FloatExtensionsTest { @Test fun testFloat_approximatelyEquals_nonZeroValues_withinInterval_returnsTrue() { val leftFloat = 1.2f - val rightFloat = leftFloat + FLOAT_EQUALITY_INTERVAL.toFloat() / 10f + val rightFloat = leftFloat + FLOAT_EQUALITY_EPSILON / 10f val result = leftFloat.approximatelyEquals(rightFloat) @@ -58,7 +58,7 @@ class FloatExtensionsTest { @Test fun testFloat_approximatelyEquals_nonZeroValues_outsideInterval_returnsFalse() { val leftFloat = 1.2f - val rightFloat = leftFloat + FLOAT_EQUALITY_INTERVAL.toFloat() * 2f + val rightFloat = leftFloat + FLOAT_EQUALITY_EPSILON * 2f val result = leftFloat.approximatelyEquals(rightFloat) @@ -97,8 +97,8 @@ class FloatExtensionsTest { @Test fun testDouble_approximatelyEquals_nonZeroValues_withinInterval_returnsTrue() { - val leftDouble = 1.2 - val rightDouble = leftDouble + FLOAT_EQUALITY_INTERVAL / 10.0 + val leftDouble = 0.2 + val rightDouble = leftDouble + DOUBLE_EQUALITY_EPSILON / 10.0 val result = leftDouble.approximatelyEquals(rightDouble) @@ -110,7 +110,7 @@ class FloatExtensionsTest { @Test fun testDouble_approximatelyEquals_nonZeroValues_outsideInterval_returnsFalse() { val leftDouble = 1.2 - val rightDouble = leftDouble + FLOAT_EQUALITY_INTERVAL * 2 + val rightDouble = leftDouble + DOUBLE_EQUALITY_EPSILON * 2 val result = leftDouble.approximatelyEquals(rightDouble) diff --git a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt index 2e13da959aa..6efbac11ed0 100644 --- a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt @@ -256,14 +256,14 @@ class RealExtensionsTest { @Test fun testIsApproximatelyEqualTo_twoAndTwoWithinThreshold_returnsTrue() { - val result = TWO_REAL.isApproximatelyEqualTo(2.0 + FLOAT_EQUALITY_INTERVAL / 2.0) + val result = TWO_REAL.isApproximatelyEqualTo(2.0 + DOUBLE_EQUALITY_EPSILON / 2.0) assertThat(result).isTrue() } @Test fun testIsApproximatelyEqualTo_twoAndTwoOutsideThreshold_returnsFalse() { - val result = TWO_REAL.isApproximatelyEqualTo(2.0 + FLOAT_EQUALITY_INTERVAL * 2.0) + val result = TWO_REAL.isApproximatelyEqualTo(2.0 + DOUBLE_EQUALITY_EPSILON * 2.0) assertThat(result).isFalse() } From 7e97d0b107294b64b48f510b9b9c325e51238bd3 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 15:39:54 -0800 Subject: [PATCH 095/134] Treat en-dash as a subtraction symbol. --- .../java/org/oppia/android/util/math/MathTokenizer.kt | 2 +- .../org/oppia/android/util/math/MathTokenizerTest.kt | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt index d45ce14d571..3f378f5de7f 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt @@ -50,7 +50,7 @@ class MathTokenizer private constructor() { '+' -> tokenizeSymbol(chars) { startIndex, endIndex -> Token.PlusSymbol(startIndex, endIndex) } - '-', '−' -> tokenizeSymbol(chars) { startIndex, endIndex -> + '-', '−', '–' -> tokenizeSymbol(chars) { startIndex, endIndex -> Token.MinusSymbol(startIndex, endIndex) } '*', '×' -> tokenizeSymbol(chars) { startIndex, endIndex -> diff --git a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt index a91ea971626..0f9abb08db1 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt @@ -369,6 +369,14 @@ class MathTokenizerTest { assertThat(tokens[0]).isMinusSymbol() } + @Test + fun testTokenize_enDashSymbol_producesMinusSymbol() { + val tokens = MathTokenizer.tokenize("–").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isMinusSymbol() + } + @Test fun testTokenize_minusSymbol_withSpaces_tokenHasCorrectIndices() { val tokens = MathTokenizer.tokenize(" − ").toList() @@ -645,7 +653,7 @@ class MathTokenizerTest { // Build a large list of unicode characters minus those which are actually allowed. The ASCII // range is excluded from this list. val characters = ('\u007f'..'\uffff').filterNot { - it in listOf('×', '÷', '−', '√') + it in listOf('×', '÷', '−', '–', '√') } val charStr = characters.joinToString("") From a9c68b1ea76e7b7bb44fa9531b5fdfc48c4a70c3 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 16:02:56 -0800 Subject: [PATCH 096/134] Add explicit platform selection for paramerized. This adds explicit platform selection support rather than it being automatic based on deps. While less flexible for shared tests, this offers better control for tests that don't want to to use Robolectric for local tests. This also adds a JUnit-only test runner, and updates MathTokenizerTest to use it (which led to an almost 40x decrease in runtime). --- .../oppia/android/testing/junit/BUILD.bazel | 14 +++++ .../junit/OppiaParameterizedBaseRunner.kt | 9 ++++ .../junit/OppiaParameterizedTestRunner.kt | 52 ++++++++----------- .../ParameterizedAndroidJUnit4ClassRunner.kt | 8 ++- .../junit/ParameterizedJunitTestRunner.kt | 45 ++++++++++++++++ .../ParameterizedRobolectricTestRunner.kt | 8 ++- .../org/oppia/android/util/math/BUILD.bazel | 2 +- .../android/util/math/MathTokenizerTest.kt | 3 ++ 8 files changed, 107 insertions(+), 34 deletions(-) create mode 100644 testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedBaseRunner.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/junit/ParameterizedJunitTestRunner.kt diff --git a/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel index d7e6d99dd25..555889937b9 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel @@ -61,6 +61,19 @@ kt_android_library( ], ) +kt_android_library( + name = "parameterized_junit_test_runner", + testonly = True, + srcs = [ + "ParameterizedJunitTestRunner.kt", + ], + visibility = ["//:oppia_testing_visibility"], + deps = [ + ":parameterized_runner_delegate_impl", + "//third_party:junit_junit", + ], +) + kt_android_library( name = "parameterized_robolectric_test_runner", testonly = True, @@ -79,6 +92,7 @@ kt_android_library( name = "parameterized_runner_delegate_impl", testonly = True, srcs = [ + "OppiaParameterizedBaseRunner.kt", "ParameterValue.kt", "ParameterizedMethod.kt", "ParameterizedRunnerDelegate.kt", diff --git a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedBaseRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedBaseRunner.kt new file mode 100644 index 00000000000..ff08c985400 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedBaseRunner.kt @@ -0,0 +1,9 @@ +package org.oppia.android.testing.junit + +/** + * This is a marker interface that's used to select a base runner to be used in conjunction with + * [OppiaParameterizedTestRunner]. + * + * See the KDoc for the test runner for more details on how to use this. + */ +interface OppiaParameterizedBaseRunner diff --git a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt index 218dc1258aa..cb67956a553 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt @@ -11,6 +11,7 @@ import org.junit.runners.Suite import java.lang.annotation.Repeatable import java.lang.reflect.Field import java.lang.reflect.Method +import kotlin.reflect.KClass /** * JUnit test runner that enables support for parameterization, that is, running a single test @@ -22,9 +23,9 @@ import java.lang.reflect.Method * use regular explicit tests, instead (since parameterized tests can hurt test maintainability and * readability). * - * This runner behaves like AndroidJUnit4 in that it should work both locally (i.e. via Robolectric) - * and on a device (i.e. with Espresso), though the correct Bazel dependency needs to be added based - * on the environment in which the test is running. + * This runner behaves like AndroidJUnit4 in that it should work in different environments based on + * which base runner is configured using [SelectRunnerPlatform] (which automatically pulls in the + * necessary Bazel dependencies). However, it will only support the platform(s) selected. * * To introduce parameterized tests, add this runner along with one or more [Parameter]-annotated * fields and one or more [RunParameterized]-annotated methods (where each method should have @@ -32,6 +33,7 @@ import java.lang.reflect.Method * * ```kotlin * @RunWith(OppiaParameterizedTestRunner::class) + * @SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) * class ExampleParameterizedTest { * @Parameter lateinit var parameter: String * @@ -73,14 +75,19 @@ import java.lang.reflect.Method */ class OppiaParameterizedTestRunner(private val testClass: Class<*>) : Suite(testClass, listOf()) { private val parameterizedMethods = computeParameterizedMethods() + private val selectedRunnerClass by lazy { fetchSelectedRunnerPlatformClass() } private val childrenRunners by lazy { // Collect all parameterized methods (for each iteration they support) plus one test runner for // all non-parameterized methods. parameterizedMethods.flatMap { (methodName, method) -> method.iterationNames.map { iterationName -> - ProxyParameterizedTestRunner(testClass, parameterizedMethods, methodName, iterationName) + ProxyParameterizedTestRunner( + selectedRunnerClass, testClass, parameterizedMethods, methodName, iterationName + ) } - } + ProxyParameterizedTestRunner(testClass, parameterizedMethods, methodName = null) + } + ProxyParameterizedTestRunner( + selectedRunnerClass, testClass, parameterizedMethods, methodName = null + ) } override fun getChildren(): MutableList = childrenRunners.toMutableList() @@ -196,6 +203,16 @@ class OppiaParameterizedTestRunner(private val testClass: Class<*>) : Suite(test } } + private fun fetchSelectedRunnerPlatformClass(): Class<*> { + return checkNotNull(testClass.getDeclaredAnnotation(SelectRunnerPlatform::class.java)) { + "All suites using OppiaParameterizedTestRunner must declare their base platform runner" + + " using SelectRunnerPlatform." + }.runnerType.java + } + + @Target(AnnotationTarget.CLASS) + annotation class SelectRunnerPlatform(val runnerType: KClass) + /** * Defines a parameter that may have an injected value that comes from per-test [Iteration] * definitions. @@ -244,6 +261,7 @@ class OppiaParameterizedTestRunner(private val testClass: Class<*>) : Suite(test ) private class ProxyParameterizedTestRunner( + private val runnerClass: Class<*>, private val testClass: Class<*>, private val parameterizedMethods: Map, private val methodName: String?, @@ -269,30 +287,6 @@ class OppiaParameterizedTestRunner(private val testClass: Class<*>) : Suite(test override fun sort(sorter: Sorter?) = delegateSortable.sort(sorter) private fun constructDelegate(): Any { - System.getProperty("android.junit.runner").also { customRunner -> - check(customRunner == null) { - "Detected a custom runner ($customRunner) in a parameterized test. This isn't yet" + - " supported." - } - } - val runningOnAndroid = - System.getProperty("java.runtime.name")?.contains("android", ignoreCase = true) ?: false - - // Load the runner class using reflection since the Robolectric implementation relies on - // Robolectric (which can't be pulled into Espresso builds of shared tests). - val runnerClass = try { - if (runningOnAndroid) { - Class.forName("org.oppia.android.testing.junit.ParameterizedAndroidJUnit4ClassRunner") - } else Class.forName("org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner") - } catch (e: Exception) { - throw IllegalStateException( - "Failed to load delegate test runner class. Did you forget to add either" + - " parameterized_android_junit4_class_runner or parameterized_robolectric_test_runner" + - " as a dependency?", - e - ) - } - val constructor = runnerClass.getConstructor( Class::class.java, Map::class.java, String::class.java, String::class.java diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt index 8526c4b557d..09dded7eff8 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt @@ -7,14 +7,18 @@ import org.junit.runners.model.Statement /** * A [AndroidJUnit4ClassRunner] which supports [OppiaParameterizedTestRunner] when running on an * Espresso-driven platform. + * + * This should be selected as the base runner when the test author wishes to use Espresso. */ @Suppress("unused") // This class is constructed using reflection. -internal class ParameterizedAndroidJUnit4ClassRunner( +class ParameterizedAndroidJUnit4ClassRunner internal constructor( testClass: Class<*>, private val parameterizedMethods: Map, private val methodName: String?, private val iterationName: String? -) : AndroidJUnit4ClassRunner(testClass), ParameterizedRunnerOverrideMethods { +) : AndroidJUnit4ClassRunner(testClass), + OppiaParameterizedBaseRunner, + ParameterizedRunnerOverrideMethods { private val delegate by lazy { ParameterizedRunnerDelegate( parameterizedMethods, diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedJunitTestRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedJunitTestRunner.kt new file mode 100644 index 00000000000..8aa5fff8c84 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedJunitTestRunner.kt @@ -0,0 +1,45 @@ +package org.oppia.android.testing.junit + +import org.junit.runners.BlockJUnit4ClassRunner +import org.junit.runners.model.FrameworkMethod +import org.junit.runners.model.Statement + +/** + * A [BlockJUnit4ClassRunner] which supports [OppiaParameterizedTestRunner] when running on a local + * JVM using JUnit directly. + * + * This should be selected as the base runner when the test author wishes to use JUnit without + * Android dependencies. This should **not** be used for Robolectric (i.e. tests that require + * Android libraries) tests; use [ParameterizedRobolectricTestRunner] for those, instead. + * + * The main advantage that this runner provides beyond the Robolectric one is that it avoids + * initializing the Android shadows that Robolectric manages. + */ +@Suppress("unused") // This class is constructed using reflection. +class ParameterizedJunitTestRunner internal constructor( + testClass: Class<*>, + private val parameterizedMethods: Map, + private val methodName: String?, + private val iterationName: String? +) : BlockJUnit4ClassRunner(testClass), + OppiaParameterizedBaseRunner, + ParameterizedRunnerOverrideMethods { + private val delegate by lazy { + ParameterizedRunnerDelegate( + parameterizedMethods, + methodName, + iterationName + ).also { delegate -> + delegate.fetchChildrenFromParent = { super.getChildren() } + delegate.fetchTestNameFromParent = { method -> super.testName(method) } + delegate.fetchMethodInvokerFromParent = { method, test -> super.methodInvoker(method, test) } + } + } + + override fun getChildren(): MutableList = delegate.getChildren() + + override fun testName(method: FrameworkMethod?): String = delegate.testName(method) + + override fun methodInvoker(method: FrameworkMethod?, test: Any?): Statement = + delegate.methodInvoker(method, test) +} diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt index 161a4bce0c2..8503933cd98 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt @@ -7,14 +7,18 @@ import org.robolectric.RobolectricTestRunner /** * A [RobolectricTestRunner] which supports [OppiaParameterizedTestRunner] when running on a local * JVM using Robolectric. + * + * This should be selected as the base runner when the test author wishes to use Robolectric. */ @Suppress("unused") // This class is constructed using reflection. -internal class ParameterizedRobolectricTestRunner( +class ParameterizedRobolectricTestRunner internal constructor( testClass: Class<*>, private val parameterizedMethods: Map, private val methodName: String?, private val iterationName: String? -) : RobolectricTestRunner(testClass), ParameterizedRunnerOverrideMethods { +) : RobolectricTestRunner(testClass), + OppiaParameterizedBaseRunner, + ParameterizedRunnerOverrideMethods { private val delegate by lazy { ParameterizedRunnerDelegate( parameterizedMethods, 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 index 079bf22babb..eb7b18b4b4f 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -68,7 +68,7 @@ oppia_android_test( "//model/src/main/proto:math_java_proto_lite", "//testing:assertion_helpers", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", - "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_junit_test_runner", "//testing/src/main/java/org/oppia/android/testing/math:token_subject", "//third_party:com_google_truth_truth", "//third_party:junit_junit", diff --git a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt index 0f9abb08db1..4cf512cb261 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt @@ -7,6 +7,8 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedJunitTestRunner import org.oppia.android.testing.math.TokenSubject.Companion.assertThat import org.robolectric.annotation.LooperMode @@ -14,6 +16,7 @@ import org.robolectric.annotation.LooperMode // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedJunitTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class MathTokenizerTest { @Parameter lateinit var variableName: String From a273ee694b623dee0753ab75f974f97e27038361 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 16:08:27 -0800 Subject: [PATCH 097/134] Exemption fixes. Also, fix name for the AndroidJUnit4 runner. --- scripts/assets/test_file_exemptions.textproto | 4 +++- .../main/java/org/oppia/android/testing/junit/BUILD.bazel | 2 +- .../android/testing/junit/OppiaParameterizedTestRunner.kt | 6 ++++++ ...assRunner.kt => ParameterizedAndroidJunit4TestRunner.kt} | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) rename testing/src/main/java/org/oppia/android/testing/junit/{ParameterizedAndroidJUnit4ClassRunner.kt => ParameterizedAndroidJunit4TestRunner.kt} (95%) diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index db2bbb8edd7..2021184308f 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -646,8 +646,10 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/Ge exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/ImageViewMatcher.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/KonfettiViewMatcher.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/DefineAppLanguageLocaleContext.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedBaseRunner.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt" -exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJunit4TestRunner.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedJunitTestRunner.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedMethod.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt" diff --git a/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel index 555889937b9..bfcac07ce7a 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel @@ -51,7 +51,7 @@ kt_android_library( name = "parameterized_android_junit4_class_runner", testonly = True, srcs = [ - "ParameterizedAndroidJUnit4ClassRunner.kt", + "ParameterizedAndroidJunit4TestRunner.kt", ], visibility = ["//:oppia_testing_visibility"], deps = [ diff --git a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt index cb67956a553..6638a801db0 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt @@ -210,6 +210,12 @@ class OppiaParameterizedTestRunner(private val testClass: Class<*>) : Suite(test }.runnerType.java } + /** + * Defines which [OppiaParameterizedBaseRunner] should be used for running individual + * parameterized and non-parameterized test cases. + * + * See base classes for options. + */ @Target(AnnotationTarget.CLASS) annotation class SelectRunnerPlatform(val runnerType: KClass) diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJunit4TestRunner.kt similarity index 95% rename from testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt rename to testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJunit4TestRunner.kt index 09dded7eff8..8ba4a5be2df 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJunit4TestRunner.kt @@ -11,7 +11,7 @@ import org.junit.runners.model.Statement * This should be selected as the base runner when the test author wishes to use Espresso. */ @Suppress("unused") // This class is constructed using reflection. -class ParameterizedAndroidJUnit4ClassRunner internal constructor( +class ParameterizedAndroidJunit4TestRunner internal constructor( testClass: Class<*>, private val parameterizedMethods: Map, private val methodName: String?, From 09e2aad0b81827cd6566f0883dc76ff81d0653da Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 16:42:14 -0800 Subject: [PATCH 098/134] Remove failing test. --- ...icInputEqualsRuleClassifierProviderTest.kt | 25 +++---------------- 1 file changed, 4 insertions(+), 21 deletions(-) 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 506df4acebe..ae70c8badc0 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 @@ -12,7 +12,7 @@ import org.junit.runner.RunWith import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows -import org.oppia.android.util.math.FLOAT_EQUALITY_EPSILON +import org.oppia.android.util.math.DOUBLE_EQUALITY_EPSILON import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject @@ -33,13 +33,11 @@ class NumericInputEqualsRuleClassifierProviderTest { private val NEGATIVE_REAL_VALUE_3_5 = InteractionObjectTestBuilder.createReal(value = -3.5) private val FIVE_TIMES_FLOAT_EQUALITY_INTERVAL = - InteractionObjectTestBuilder.createReal(value = 5 * FLOAT_EQUALITY_EPSILON) - private val SIX_TIMES_FLOAT_EQUALITY_INTERVAL = - InteractionObjectTestBuilder.createReal(value = 6 * FLOAT_EQUALITY_EPSILON) + InteractionObjectTestBuilder.createReal(value = 5 * DOUBLE_EQUALITY_EPSILON) private val FIVE_POINT_ONE_TIMES_FLOAT_EQUALITY_INTERVAL = InteractionObjectTestBuilder.createReal( - value = 5 * FLOAT_EQUALITY_EPSILON + - FLOAT_EQUALITY_EPSILON / 10 + value = 5 * DOUBLE_EQUALITY_EPSILON + + DOUBLE_EQUALITY_EPSILON / 10 ) private val STRING_VALUE = InteractionObjectTestBuilder.createString(value = "test") @@ -142,21 +140,6 @@ class NumericInputEqualsRuleClassifierProviderTest { assertThat(matches).isFalse() } - @Test - fun testPositiveRealAnswer_positiveRealInput_valueAtRange_valuesDoNotMatch() { - val inputs = mapOf( - "x" to FIVE_TIMES_FLOAT_EQUALITY_INTERVAL - ) - - val matches = inputEqualsRuleClassifier.matches( - answer = SIX_TIMES_FLOAT_EQUALITY_INTERVAL, - inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() - ) - - assertThat(matches).isFalse() - } - @Test fun testRealAnswer_missingInput_throwsException() { val inputs = mapOf("y" to POSITIVE_REAL_VALUE_1_5) From b48d06a78b3ed487201b62070d34fc8ce889cfd2 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 17:00:29 -0800 Subject: [PATCH 099/134] Fix unary expression precedence. Also, use ParameterizedJunitTestRunner for MathExpressionParserTest. --- .../android/util/math/MathExpressionParser.kt | 8 +- .../math/AlgebraicExpressionParserTest.kt | 66 ++++++------- .../org/oppia/android/util/math/BUILD.bazel | 2 +- .../util/math/MathExpressionParserTest.kt | 3 + .../util/math/NumericExpressionParserTest.kt | 97 ++++++++++++++++--- 5 files changed, 123 insertions(+), 53 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt index 20b44cee192..c04120a7d02 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -453,9 +453,9 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } private fun parseGenericNegatedTerm(): MathParsingResult { - // generic_negated_term = minus_operator , generic_mult_div_expression ; + // generic_negated_term = minus_operator , generic_exp_expression ; val minusResult = parseContext.consumeTokenOfType() - val expResult = minusResult.flatMap { parseGenericMultDivExpression() } + val expResult = minusResult.flatMap { parseGenericExpExpression() } return minusResult.combineWith(expResult) { minus, op -> MathExpression.newBuilder().apply { parseStartIndex = minus.startIndex @@ -469,9 +469,9 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } private fun parseGenericPositiveTerm(): MathParsingResult { - // generic_positive_term = plus_operator , generic_mult_div_expression ; + // generic_positive_term = plus_operator , generic_exp_expression ; val plusResult = parseContext.consumeTokenOfType() - val expResult = plusResult.flatMap { parseGenericMultDivExpression() } + val expResult = plusResult.flatMap { parseGenericExpExpression() } return plusResult.combineWith(expResult) { plus, op -> MathExpression.newBuilder().apply { parseStartIndex = plus.startIndex diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt index be190a7a520..3c327a362cc 100644 --- a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt @@ -488,19 +488,19 @@ class AlgebraicExpressionParserTest { // Similar to the previous test, but this ensures negation ordering (relative to variables & // implicit multiplication). assertThat(expression).hasStructureThatMatches { - negation { - operand { - multiplication(isImplicit = true) { - leftOperand { + multiplication(isImplicit = true) { + leftOperand { + negation { + operand { constant { withValueThat().isIntegerThat().isEqualTo(2) } } - rightOperand { - variable { - withNameThat().isEqualTo("x") - } - } + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") } } } @@ -687,41 +687,41 @@ class AlgebraicExpressionParserTest { // This combines all of the distinct pieces tested earlier to demonstrate full polynomial // syntax. assertThat(expression).hasStructureThatMatches { - // -8x^3-7.4x^2+x-12√2 -> (((-(8*(x^3))) - (7.4*(x^2))) + x) - (12/√2) + // -8x^3-7.4x^2+x-12/√2 -> ((((-8) * (x^3)) - (7.4 * (x^2))) + x) - (12/√2) subtraction { leftOperand { - // ((-(8*(x^3))) - (7.4*(x^2))) + x + // (((-8) * (x^3)) - (7.4 * (x^2))) + x addition { leftOperand { - // (-(8*(x^3))) - (7.4*(x^2)) + // ((-8) * (x^3)) - (7.4 * (x^2)) subtraction { leftOperand { - // -(8*(x^3)) - negation { - operand { - // 8*(x^3) - multiplication(isImplicit = true) { - leftOperand { + // (-8) * (x^3) + multiplication(isImplicit = true) { + leftOperand { + // -8 + negation { + operand { // 8 constant { withValueThat().isIntegerThat().isEqualTo(8) } } + } + } + rightOperand { + // x^3 + exponentiation { + leftOperand { + // x + variable { + withNameThat().isEqualTo("x") + } + } rightOperand { - // x^3 - exponentiation { - leftOperand { - // x - variable { - withNameThat().isEqualTo("x") - } - } - rightOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -729,7 +729,7 @@ class AlgebraicExpressionParserTest { } } rightOperand { - // 7.4*(x^2) + // 7.4 * (x^2) multiplication(isImplicit = true) { leftOperand { // 7.4 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 index 063039f99d1..217f16638ab 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -105,7 +105,7 @@ oppia_android_test( deps = [ "//model/src/main/proto:math_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", - "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_junit_test_runner", "//testing/src/main/java/org/oppia/android/testing/math:math_parsing_error_subject", "//third_party:com_google_truth_extensions_truth-liteproto-extension", "//third_party:com_google_truth_truth", diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt index 198d24551b4..4a00234fb76 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -14,6 +14,8 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedJunitTestRunner import org.oppia.android.testing.math.MathParsingErrorSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS @@ -40,6 +42,7 @@ import org.robolectric.annotation.LooperMode // SameParameterValue: tests should have specific context included/excluded for readability. @Suppress("FunctionName", "SameParameterValue") @RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedJunitTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class MathExpressionParserTest { @Parameter lateinit var lhsOp: String diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index 93628887ae7..c645fcd1636 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -413,7 +413,7 @@ class NumericExpressionParserTest { fun testParse_negationAndExponentiation_returnsExpWithNegationResolvedLast() { val expression = parseNumericExpressionWithAllErrors("-3^4") - // Exponentiation is resolved first since negation is lower precedent. + // Exponentiation is resolved first since negation is higher precedence. assertThat(expression).hasStructureThatMatches { negation { operand { @@ -434,6 +434,74 @@ class NumericExpressionParserTest { } } + @Test + fun testParse_exponentiationAndNegatedMultiplication_returnsExpWithMultiplicationResolvedLast() { + val expression = parseNumericExpressionWithAllErrors("10^-5*3") + + // Negation is isolated since multiplication is higher precedence. + assertThat(expression).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(10) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + @Test + fun testParse_exponentiationAndPositiveMultiplication_returnsExpWithMultiplicationResolvedLast() { + val expression = parseNumericExpressionWithoutOptionalErrors("10^+5*3") + + // Positive is isolated since multiplication is higher precedence. + assertThat(expression).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(10) + } + } + rightOperand { + positive { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + @Test fun testParse_inlineSquareRootAndExponentiation_returnsExpWithSquareRootResolvedFirst() { val expression = parseNumericExpressionWithAllErrors("√3^4") @@ -1364,25 +1432,24 @@ class NumericExpressionParserTest { fun testParse_multiplicationOfNegations_returnsExpWithCorrectStructure() { val expression = parseNumericExpressionWithAllErrors("-2*-3") - // Note that the following structure is not the same as (-2)*(-2) since unary negation has - // lower precedence than multiplication, so it's computed as first with its operand being the - // multiplication expression. + // Note that the following structure is the same as (-2)*(-2) since unary negation has higher + // precedence than multiplication. assertThat(expression).hasStructureThatMatches { - negation { - operand { - multiplication { - leftOperand { + multiplication { + leftOperand { + negation { + operand { constant { withValueThat().isIntegerThat().isEqualTo(2) } } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) } } } From 8ac22cb7eec13d5a14e36e7fd2c62f35561747bf Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 17:05:08 -0800 Subject: [PATCH 100/134] Fixes & add more test cases. --- .../util/math/MathExpressionParserTest.kt | 41 ++++++++++++------- .../util/math/NumericExpressionParserTest.kt | 20 +++++++++ 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt index 4a00234fb76..89e14517e5e 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -45,11 +45,16 @@ import org.robolectric.annotation.LooperMode @SelectRunnerPlatform(ParameterizedJunitTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class MathExpressionParserTest { - @Parameter lateinit var lhsOp: String - @Parameter lateinit var rhsOp: String - @Parameter lateinit var binOp: String - @Parameter lateinit var subExp: String - @Parameter lateinit var func: String + @Parameter + lateinit var lhsOp: String + @Parameter + lateinit var rhsOp: String + @Parameter + lateinit var binOp: String + @Parameter + lateinit var subExp: String + @Parameter + lateinit var func: String @Test fun testParseNumExp_basicExpression_doesNotFail() { @@ -546,22 +551,25 @@ class MathExpressionParserTest { Iteration("/*", "lhsOp=/", "rhsOp=*"), Iteration("÷*", "lhsOp=÷", "rhsOp=*"), Iteration("^*", "lhsOp=^", "rhsOp=*"), Iteration("+*", "lhsOp=+", "rhsOp=*"), Iteration("-*", "lhsOp=-", "rhsOp=*"), Iteration("−*", "lhsOp=−", "rhsOp=*"), - Iteration("*×", "lhsOp=*", "rhsOp=×"), Iteration("××", "lhsOp=×", "rhsOp=×"), - Iteration("/×", "lhsOp=/", "rhsOp=×"), Iteration("÷×", "lhsOp=÷", "rhsOp=×"), - Iteration("^×", "lhsOp=^", "rhsOp=×"), Iteration("+×", "lhsOp=+", "rhsOp=×"), - Iteration("-×", "lhsOp=-", "rhsOp=×"), Iteration("−×", "lhsOp=−", "rhsOp=×"), + Iteration("–*", "lhsOp=–", "rhsOp=*"), Iteration("*×", "lhsOp=*", "rhsOp=×"), + Iteration("××", "lhsOp=×", "rhsOp=×"), Iteration("/×", "lhsOp=/", "rhsOp=×"), + Iteration("÷×", "lhsOp=÷", "rhsOp=×"), Iteration("^×", "lhsOp=^", "rhsOp=×"), + Iteration("+×", "lhsOp=+", "rhsOp=×"), Iteration("-×", "lhsOp=-", "rhsOp=×"), + Iteration("−×", "lhsOp=−", "rhsOp=×"), Iteration("–×", "lhsOp=–", "rhsOp=×"), Iteration("*/", "lhsOp=*", "rhsOp=/"), Iteration("×/", "lhsOp=×", "rhsOp=/"), Iteration("//", "lhsOp=/", "rhsOp=/"), Iteration("÷/", "lhsOp=÷", "rhsOp=/"), Iteration("^/", "lhsOp=^", "rhsOp=/"), Iteration("+/", "lhsOp=+", "rhsOp=/"), Iteration("-/", "lhsOp=-", "rhsOp=/"), Iteration("−/", "lhsOp=−", "rhsOp=/"), - Iteration("*÷", "lhsOp=*", "rhsOp=÷"), Iteration("×÷", "lhsOp=×", "rhsOp=÷"), - Iteration("/÷", "lhsOp=/", "rhsOp=÷"), Iteration("÷÷", "lhsOp=÷", "rhsOp=÷"), - Iteration("^÷", "lhsOp=^", "rhsOp=÷"), Iteration("+÷", "lhsOp=+", "rhsOp=÷"), - Iteration("-÷", "lhsOp=-", "rhsOp=÷"), Iteration("−÷", "lhsOp=−", "rhsOp=÷"), + Iteration("–/", "lhsOp=–", "rhsOp=/"), Iteration("*÷", "lhsOp=*", "rhsOp=÷"), + Iteration("×÷", "lhsOp=×", "rhsOp=÷"), Iteration("/÷", "lhsOp=/", "rhsOp=÷"), + Iteration("÷÷", "lhsOp=÷", "rhsOp=÷"), Iteration("^÷", "lhsOp=^", "rhsOp=÷"), + Iteration("+÷", "lhsOp=+", "rhsOp=÷"), Iteration("-÷", "lhsOp=-", "rhsOp=÷"), + Iteration("−÷", "lhsOp=−", "rhsOp=÷"), Iteration("–÷", "lhsOp=–", "rhsOp=÷"), Iteration("*^", "lhsOp=*", "rhsOp=^"), Iteration("×^", "lhsOp=×", "rhsOp=^"), Iteration("/^", "lhsOp=/", "rhsOp=^"), Iteration("÷^", "lhsOp=÷", "rhsOp=^"), Iteration("^^", "lhsOp=^", "rhsOp=^"), Iteration("+^", "lhsOp=+", "rhsOp=^"), - Iteration("-^", "lhsOp=-", "rhsOp=^"), Iteration("−^", "lhsOp=−", "rhsOp=^") + Iteration("-^", "lhsOp=-", "rhsOp=^"), Iteration("−^", "lhsOp=−", "rhsOp=^"), + Iteration("–^", "lhsOp=–", "rhsOp=^") ) fun testParseNumExp_adjacentBinaryOps_returnsSubsequentBinaryOperatorsErrorWithDetails() { val expression = "1 $lhsOp$rhsOp 2" @@ -696,6 +704,7 @@ class MathExpressionParserTest { Iteration("something_to_power_of_nothing", "binOp=^"), Iteration("something_adds_nothing", "binOp=+"), Iteration("something_subtracts_nothing_hyphen", "binOp=-"), + Iteration("something_subtracts_nothing_en_dash", "binOp=–"), Iteration("something_subtracts_nothing", "binOp=−") ) fun testParseNumExp_binaryOps_noRightValue_returnsNoVarOrNumAfterBinOperatorErrorWithDetails() { @@ -720,6 +729,7 @@ class MathExpressionParserTest { Iteration("something_to_power_of_nothing", "binOp=^"), Iteration("something_adds_nothing", "binOp=+"), Iteration("something_subtracts_nothing_hyphen", "binOp=-"), + Iteration("something_subtracts_nothing_en_dash", "binOp=–"), Iteration("something_subtracts_nothing", "binOp=−") ) fun testParseAlgExp_binaryOps_noRightValue_returnsNoVarOrNumAfterBinOperatorErrorWithDetails() { @@ -1086,7 +1096,8 @@ class MathExpressionParserTest { "^" to EXPONENTIATE, "+" to ADD, "-" to SUBTRACT, - "−" to SUBTRACT + "−" to SUBTRACT, + "–" to SUBTRACT ) private val LOWERCASE_LATIN_ALPHABET = listOf( "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index c645fcd1636..16933425c25 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -129,6 +129,26 @@ class NumericExpressionParserTest { } } + @Test + fun testParse_subtraction_withEnDashSymbol_returnsExpressionWithBinaryOperation() { + val expression = parseNumericExpressionWithAllErrors("1 – 2") + + assertThat(expression).hasStructureThatMatches { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + @Test fun testParse_multiplication_returnsExpressionWithBinaryOperation() { val expression = parseNumericExpressionWithAllErrors("1 * 2") From 476e6043191985e7342d17241a97a5b421c76d15 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 17:15:23 -0800 Subject: [PATCH 101/134] Post-merge fixes & test changes. Also, update RealExtensionsTest to use the faster JUnit runner. --- .../src/test/java/org/oppia/android/util/math/BUILD.bazel | 2 +- .../android/util/math/NumericExpressionParserTest.kt | 8 ++++++++ .../org/oppia/android/util/math/RealExtensionsTest.kt | 3 +++ 3 files changed, 12 insertions(+), 1 deletion(-) 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 index 625bb3e5e12..56d3e6ab02c 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -275,7 +275,7 @@ oppia_android_test( "//model/src/main/proto:math_java_proto_lite", "//testing", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", - "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_junit_test_runner", "//testing/src/main/java/org/oppia/android/testing/math:real_subject", "//third_party:com_google_truth_truth", "//third_party:junit_junit", diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index d6241fe75ae..37e89515be3 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -160,6 +160,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(-1) } @Test @@ -526,6 +527,12 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(3) + hasDenominatorThat().isEqualTo(100000) + } } @Test @@ -560,6 +567,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(300000) } @Test diff --git a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt index a218f852769..744784c355b 100644 --- a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt @@ -10,6 +10,8 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedJunitTestRunner import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.robolectric.annotation.LooperMode @@ -17,6 +19,7 @@ import org.robolectric.annotation.LooperMode // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedJunitTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class RealExtensionsTest { private companion object { From 5ed0013ee47b517ad8e091b455ed366f0d4acc44 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 17:33:22 -0800 Subject: [PATCH 102/134] Use utility directly in LaTeX tests. --- .../org/oppia/android/util/math/BUILD.bazel | 3 ++ .../org/oppia/android/util/math/BUILD.bazel | 2 +- .../math/ExpressionToLatexConverterTest.kt | 43 ++++++++++--------- 3 files changed, 26 insertions(+), 22 deletions(-) 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 index 61cb0d11d08..4ae833ce0b7 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -156,6 +156,9 @@ kt_android_library( srcs = [ "ExpressionToLatexConverter.kt", ], + visibility = [ + "//:oppia_testing_visibility", + ], deps = [ ":real_extensions", "//model/src/main/proto:math_java_proto_lite", 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 index 56d3e6ab02c..7021569bf26 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -56,7 +56,7 @@ oppia_android_test( "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:extensions", + "//utility/src/main/java/org/oppia/android/util/math:expression_to_latex_converter", "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt index d9125d519a5..e351f9fc6d9 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt @@ -6,6 +6,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression +import org.oppia.android.util.math.ExpressionToLatexConverter.Companion.convertToLatex import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode @@ -21,7 +22,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_number_returnsConstantLatex() { val exp = parseNumericExpressionWithAllErrors("1") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("1") } @@ -30,7 +31,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_unaryPlus_withoutOptionalErrors_returnLatexWithUnaryPlus() { val exp = parseNumericExpressionWithoutOptionalErrors("+1") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("+1") } @@ -39,7 +40,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_unaryMinus_returnLatexWithUnaryMinus() { val exp = parseNumericExpressionWithAllErrors("-1") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("-1") } @@ -48,7 +49,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_addition_returnsLatexWithAddition() { val exp = parseNumericExpressionWithAllErrors("1+2") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("1 + 2") } @@ -57,7 +58,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_subtraction_returnsLatexWithSubtract() { val exp = parseNumericExpressionWithAllErrors("1-2") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("1 - 2") } @@ -66,7 +67,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_multiplication_returnsLatexWithMultiplication() { val exp = parseNumericExpressionWithAllErrors("2*3") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("2 \\times 3") } @@ -75,7 +76,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_division_returnsLatexWithDivision() { val exp = parseNumericExpressionWithAllErrors("2/3") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("2 \\div 3") } @@ -84,7 +85,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_division_divAsFraction_returnsLatexWithFraction() { val exp = parseNumericExpressionWithAllErrors("2/3") - val latex = exp.toRawLatex(divAsFraction = true) + val latex = exp.convertToLatex(divAsFraction = true) assertThat(latex).isEqualTo("\\frac{2}{3}") } @@ -93,7 +94,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_multipleDivisions_divAsFraction_returnsLatexWithFractions() { val exp = parseNumericExpressionWithAllErrors("2/3/4") - val latex = exp.toRawLatex(divAsFraction = true) + val latex = exp.convertToLatex(divAsFraction = true) assertThat(latex).isEqualTo("\\frac{\\frac{2}{3}}{4}") } @@ -102,7 +103,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_exponent_returnsLatexWithExponent() { val exp = parseNumericExpressionWithAllErrors("2^3") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("2 ^ {3}") } @@ -111,7 +112,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_inlineSquareRoot_returnsLatexWithSquareRoot() { val exp = parseNumericExpressionWithAllErrors("√2") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("\\sqrt{2}") } @@ -120,7 +121,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_inlineSquareRoot_operationArg_returnsLatexWithSquareRoot() { val exp = parseNumericExpressionWithAllErrors("√(1+2)") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("\\sqrt{(1 + 2)}") } @@ -129,7 +130,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_squareRoot_returnsLatexWithSquareRoot() { val exp = parseNumericExpressionWithAllErrors("sqrt(2)") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("\\sqrt{2}") } @@ -138,7 +139,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_squareRoot_operationArg_returnsLatexWithSquareRoot() { val exp = parseNumericExpressionWithAllErrors("sqrt(1+2)") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("\\sqrt{1 + 2}") } @@ -147,7 +148,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_parentheses_returnsLatexWithGroup() { val exp = parseNumericExpressionWithAllErrors("2/(3+4)") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("2 \\div (3 + 4)") } @@ -156,7 +157,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_exponentToGroup_returnsCorrectlyWrappedLatex() { val exp = parseNumericExpressionWithAllErrors("2^(7-3)") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("2 ^ {(7 - 3)}") } @@ -165,7 +166,7 @@ class ExpressionToLatexConverterTest { fun testConvert_algebraicExp_variable_returnsVariableLatex() { val exp = parseAlgebraicExpressionWithAllErrors("x") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("x") } @@ -174,7 +175,7 @@ class ExpressionToLatexConverterTest { fun testConvert_algebraicExp_twoX_returnsLatexWithImplicitMultiplication() { val exp = parseAlgebraicExpressionWithAllErrors("2x") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("2x") } @@ -183,7 +184,7 @@ class ExpressionToLatexConverterTest { fun testConvert_algebraicEq_xEqualsOne_returnsLatexWithEquals() { val exp = parseAlgebraicEquationWithAllErrors("x=1") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("x = 1") } @@ -192,7 +193,7 @@ class ExpressionToLatexConverterTest { fun testConvert_algebraicEq_complexExpression_returnsCorrectLatex() { val exp = parseAlgebraicEquationWithAllErrors("(x+1)(x-2)=(x^3+2x^2-5x-6)/(x+3)") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("(x + 1)(x - 2) = (x ^ {3} + 2x ^ {2} - 5x - 6) \\div (x + 3)") } @@ -201,7 +202,7 @@ class ExpressionToLatexConverterTest { fun testConvert_algebraicEq_complexExpression_divAsFraction_returnsCorrectLatex() { val exp = parseAlgebraicEquationWithAllErrors("(x+1)(x-2)=(x^3+2x^2-5x-6)/(x+3)") - val latex = exp.toRawLatex(divAsFraction = true) + val latex = exp.convertToLatex(divAsFraction = true) assertThat(latex).isEqualTo("(x + 1)(x - 2) = \\frac{(x ^ {3} + 2x ^ {2} - 5x - 6)}{(x + 3)}") } From c16db644bf3e13a8cbbdacb02b09996651ce3fd2 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 17:40:20 -0800 Subject: [PATCH 103/134] Post-merge fixes. Also, update ExpressionToComparableOperationConverterTest to use the fast JUnit-only runner. --- utility/src/test/java/org/oppia/android/util/math/BUILD.bazel | 2 +- .../util/math/ExpressionToComparableOperationConverterTest.kt | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) 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 index 2f80dd6025b..4ba0812e29f 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -68,7 +68,7 @@ oppia_android_test( deps = [ "//model/src/main/proto:math_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", - "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_junit_test_runner", "//testing/src/main/java/org/oppia/android/testing/math:comparable_operation_subject", "//third_party:com_google_truth_truth", "//third_party:junit_junit", diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt index 4af4b5afaa9..203ad029c5b 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt @@ -11,6 +11,8 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedJunitTestRunner import org.oppia.android.testing.math.ComparableOperationSubject.Companion.assertThat import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.convertToComparableOperation import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode @@ -28,6 +30,7 @@ import org.robolectric.annotation.LooperMode // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedJunitTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class ExpressionToComparableOperationConverterTest { @Parameter lateinit var op1: String From dab330eb1d9ac117859856ecce0519897e2e95cb Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 17:50:04 -0800 Subject: [PATCH 104/134] Post-merge fixes. Also, update PolynomialExtensionsTest to use fast JUnit-only runner. --- utility/src/test/java/org/oppia/android/util/math/BUILD.bazel | 2 +- .../org/oppia/android/util/math/PolynomialExtensionsTest.kt | 3 +++ .../java/org/oppia/android/util/math/RealExtensionsTest.kt | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) 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 index 050fb4b94b4..0f7c78a736c 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -295,7 +295,7 @@ oppia_android_test( deps = [ "//model/src/main/proto:math_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", - "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_junit_test_runner", "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", "//third_party:com_google_truth_truth", "//third_party:junit_junit", diff --git a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt index 36b1aaa5224..2f8813d8096 100644 --- a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt @@ -12,6 +12,8 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedJunitTestRunner import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.robolectric.annotation.LooperMode @@ -20,6 +22,7 @@ import org.robolectric.annotation.LooperMode // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedJunitTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class PolynomialExtensionsTest { private companion object { diff --git a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt index 49713266d7e..b6a13238005 100644 --- a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt @@ -418,7 +418,7 @@ class RealExtensionsTest { @Test fun testIsApproximatelyZero_irrationalCloseToZero_returnsTrue() { - val real = createIrrationalReal(0.000000001) + val real = createIrrationalReal(0.00000000000000001) val result = real.isApproximatelyZero() From 9c70c31f643a58e7d1bcbb5761b2f69b27a0c876 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 18:13:42 -0800 Subject: [PATCH 105/134] Post-merge fixes. Also, update float interval per new tests. --- ...sEquivalentToRuleClassifierProviderTest.kt | 209 +++++++++--------- ...esExactlyWithRuleClassifierProviderTest.kt | 3 + ...ManipulationsRuleClassifierProviderTest.kt | 3 + .../android/util/math/FloatExtensions.kt | 5 +- .../org/oppia/android/util/math/BUILD.bazel | 2 +- .../math/ComparableOperationExtensionsTest.kt | 2 +- .../android/util/math/FloatExtensionsTest.kt | 23 +- .../util/math/MathExpressionExtensionsTest.kt | 7 +- .../util/math/PolynomialExtensionsTest.kt | 4 +- .../android/util/math/RealExtensionsTest.kt | 24 +- 10 files changed, 149 insertions(+), 133 deletions(-) diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt index 3f9b00babce..2d2aee241b2 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt @@ -20,6 +20,8 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule @@ -32,6 +34,7 @@ import org.robolectric.annotation.LooperMode // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class NumericExpressionInputIsEquivalentToRuleClassifierProviderTest { @@ -187,110 +190,110 @@ class NumericExpressionInputIsEquivalentToRuleClassifierProviderTest { @Test @RunParameterized( -// Iteration("2*(2+6+3+4)==2*(2+6+3+4)", "answer=2*(2+6+3+4)", "input=2*(2+6+3+4)"), -// Iteration("2 × (2+6+3+4)==2*(2+6+3+4)", "answer=2 × (2+6+3+4)", "input=2*(2+6+3+4)"), -// Iteration( -// "15 - (6 × 2) + 3==15 - (6 × 2) + 3", "answer=15 - (6 × 2) + 3", "input=15 - (6 × 2) + 3" -// ), -// Iteration( -// "2 × (50 + 150 + 100 + 25) ==(50 + 150 + 100 + 25) × 2", -// "answer=2 × (50 + 150 + 100 + 25) ", -// "input=(50 + 150 + 100 + 25) × 2" -// ), -// Iteration( -// "2 * (50 + 150 + 100 + 25) ==2 × (50 + 150 + 100 + 25)", -// "answer=2 * (50 + 150 + 100 + 25) ", -// "input=2 × (50 + 150 + 100 + 25)" -// ), -// Iteration("2+5==5+2", "answer=2+5", "input=5+2"), -// Iteration("5+2==5+2", "answer=5+2", "input=5+2"), -// Iteration("6 − (− 4)==6 − (− 4)", "answer=6 − (− 4)", "input=6 − (− 4)"), -// Iteration("6-(-4)==6 − (− 4)", "answer=6-(-4)", "input=6 − (− 4)"), -// Iteration("− (− 4) + 6==6 − (− 4)", "answer=− (− 4) + 6", "input=6 − (− 4)"), -// Iteration("10==6 − (− 4)", "answer=10", "input=6 − (− 4)"), -// Iteration("6 + 4==6 − (− 4)", "answer=6 + 4", "input=6 − (− 4)"), -// Iteration("6 + 2^2==6 − (− 4)", "answer=6 + 2^2", "input=6 − (− 4)"), -// Iteration("3 * 2 − (− 4)==6 − (− 4)", "answer=3 * 2 − (− 4)", "input=6 − (− 4)"), -// Iteration("100/10==6 − (− 4)", "answer=100/10", "input=6 − (− 4)"), -// Iteration("3 * 10^-5==3 * 10^-5", "answer=3 * 10^-5", "input=3 * 10^-5"), -// Iteration("3/(10 * 10^4)==3 * 10^-5", "answer=3/(10 * 10^4)", "input=3 * 10^-5"), -// Iteration( -// "1000 + 200 + 30 + 4 + 0.5 + 0.06==1000 + 200 + 30 + 4 + 0.5 + 0.06", -// "answer=1000 + 200 + 30 + 4 + 0.5 + 0.06", -// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" -// ), -// Iteration( -// "200 + 30 + 4 + 0.5 + 0.06 + 1000==1000 + 200 + 30 + 4 + 0.5 + 0.06", -// "answer=200 + 30 + 4 + 0.5 + 0.06 + 1000", -// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" -// ), -// Iteration( -// "0.06 + 0.5 + 4 + 30 + 200 + 1000==1000 + 200 + 30 + 4 + 0.5 + 0.06", -// "answer=0.06 + 0.5 + 4 + 30 + 200 + 1000", -// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" -// ), -// Iteration( -// "1234.56==1000 + 200 + 30 + 4 + 0.5 + 0.06", -// "answer=1234.56", -// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" -// ), -// Iteration( -// "123456/100==1000 + 200 + 30 + 4 + 0.5 + 0.06", -// "answer=123456/100", -// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" -// ), -// Iteration( -// "61728/50==1000 + 200 + 30 + 4 + 0.5 + 0.06", -// "answer=61728/50", -// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" -// ), -// Iteration( -// "1234 + 56/100==1000 + 200 + 30 + 4 + 0.5 + 0.06", -// "answer=1234 + 56/100", -// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" -// ), -// Iteration( -// "1230 + 4.56==1000 + 200 + 30 + 4 + 0.5 + 0.06", -// "answer=1230 + 4.56", -// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" -// ), -// Iteration("2 * 2 * 3 * 3==2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3", "input=2 * 2 * 3 * 3"), -// Iteration( -// "2 * 2 * 3 * 3 * 1==2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3 * 1", "input=2 * 2 * 3 * 3" -// ), -// Iteration("2 * 2 * 9==2 * 2 * 3 * 3", "answer=2 * 2 * 9", "input=2 * 2 * 3 * 3"), -// Iteration("4 * 3^2==2 * 2 * 3 * 3", "answer=4 * 3^2", "input=2 * 2 * 3 * 3"), -// Iteration("8/2 * 3 * 3==2 * 2 * 3 * 3", "answer=8/2 * 3 * 3", "input=2 * 2 * 3 * 3"), -// Iteration("36==2 * 2 * 3 * 3", "answer=36", "input=2 * 2 * 3 * 3"), -// Iteration("sqrt(4-2)==sqrt(2)", "answer=sqrt(4-2)", "input=sqrt(2)"), -// Iteration("2*(6+3+4) + 4==2*(2+6+3+4)", "answer=2*(6+3+4) + 4", "input=2*(2+6+3+4)"), -// Iteration("2*(2+6+3) + 8==2*(2+6+3+4)", "answer=2*(2+6+3) + 8", "input=2*(2+6+3+4)"), -// Iteration("(2+6+3+4)*2==2*(2+6+3+4)", "answer=(2+6+3+4)*2", "input=2*(2+6+3+4)"), -// Iteration("(2+6+3+4) × 2==2*(2+6+3+4)", "answer=(2+6+3+4) × 2", "input=2*(2+6+3+4)"), -// Iteration("15 - 12 + 3==15 - (6 × 2) + 3", "answer=15 - 12 + 3", "input=15 - (6 × 2) + 3"), -// Iteration( -// "3 - (6 * 2) + 15==15 - (6 × 2) + 3", "answer=3 - (6 * 2) + 15", "input=15 - (6 × 2) + 3" -// ), -// Iteration( -// "15 - (2 × 6) + 3==15 - (6 × 2) + 3", "answer=15 - (2 × 6) + 3", "input=15 - (6 × 2) + 3" -// ), -// Iteration( -// "2 *(50 + 150) + 2*(100 + 25)==(50 + 150 + 100 + 25) × 2", -// "answer=2 *(50 + 150) + 2*(100 + 25)", -// "input=(50 + 150 + 100 + 25) × 2" -// ), -// Iteration( -// "2* ( 25+50+100+150)==(50 + 150 + 100 + 25) × 2", -// "answer=2* ( 25+50+100+150)", -// "input=(50 + 150 + 100 + 25) × 2" -// ), -// Iteration("10^−5 * 3==3 * 10^-5", "answer=10^−5 * 3", "input=3 * 10^-5"), -// Iteration("30 * 10^−6==3 * 10^-5", "answer=30 * 10^−6", "input=3 * 10^-5"), -// Iteration("0.00003==3 * 10^-5", "answer=0.00003", "input=3 * 10^-5"), -// Iteration("3/10^5==3 * 10^-5", "answer=3/10^5", "input=3 * 10^-5"), + Iteration("2*(2+6+3+4)==2*(2+6+3+4)", "answer=2*(2+6+3+4)", "input=2*(2+6+3+4)"), + Iteration("2 × (2+6+3+4)==2*(2+6+3+4)", "answer=2 × (2+6+3+4)", "input=2*(2+6+3+4)"), + Iteration( + "15 - (6 × 2) + 3==15 - (6 × 2) + 3", "answer=15 - (6 × 2) + 3", "input=15 - (6 × 2) + 3" + ), + Iteration( + "2 × (50 + 150 + 100 + 25) ==(50 + 150 + 100 + 25) × 2", + "answer=2 × (50 + 150 + 100 + 25) ", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration( + "2 * (50 + 150 + 100 + 25) ==2 × (50 + 150 + 100 + 25)", + "answer=2 * (50 + 150 + 100 + 25) ", + "input=2 × (50 + 150 + 100 + 25)" + ), + Iteration("2+5==5+2", "answer=2+5", "input=5+2"), + Iteration("5+2==5+2", "answer=5+2", "input=5+2"), + Iteration("6 − (− 4)==6 − (− 4)", "answer=6 − (− 4)", "input=6 − (− 4)"), + Iteration("6-(-4)==6 − (− 4)", "answer=6-(-4)", "input=6 − (− 4)"), + Iteration("− (− 4) + 6==6 − (− 4)", "answer=− (− 4) + 6", "input=6 − (− 4)"), + Iteration("10==6 − (− 4)", "answer=10", "input=6 − (− 4)"), + Iteration("6 + 4==6 − (− 4)", "answer=6 + 4", "input=6 − (− 4)"), + Iteration("6 + 2^2==6 − (− 4)", "answer=6 + 2^2", "input=6 − (− 4)"), + Iteration("3 * 2 − (− 4)==6 − (− 4)", "answer=3 * 2 − (− 4)", "input=6 − (− 4)"), + Iteration("100/10==6 − (− 4)", "answer=100/10", "input=6 − (− 4)"), + Iteration("3 * 10^-5==3 * 10^-5", "answer=3 * 10^-5", "input=3 * 10^-5"), + Iteration("3/(10 * 10^4)==3 * 10^-5", "answer=3/(10 * 10^4)", "input=3 * 10^-5"), + Iteration( + "1000 + 200 + 30 + 4 + 0.5 + 0.06==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "200 + 30 + 4 + 0.5 + 0.06 + 1000==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=200 + 30 + 4 + 0.5 + 0.06 + 1000", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "0.06 + 0.5 + 4 + 30 + 200 + 1000==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=0.06 + 0.5 + 4 + 30 + 200 + 1000", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1234.56==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1234.56", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "123456/100==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=123456/100", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "61728/50==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=61728/50", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1234 + 56/100==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1234 + 56/100", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1230 + 4.56==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1230 + 4.56", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration("2 * 2 * 3 * 3==2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration( + "2 * 2 * 3 * 3 * 1==2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3 * 1", "input=2 * 2 * 3 * 3" + ), + Iteration("2 * 2 * 9==2 * 2 * 3 * 3", "answer=2 * 2 * 9", "input=2 * 2 * 3 * 3"), + Iteration("4 * 3^2==2 * 2 * 3 * 3", "answer=4 * 3^2", "input=2 * 2 * 3 * 3"), + Iteration("8/2 * 3 * 3==2 * 2 * 3 * 3", "answer=8/2 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration("36==2 * 2 * 3 * 3", "answer=36", "input=2 * 2 * 3 * 3"), + Iteration("sqrt(4-2)==sqrt(2)", "answer=sqrt(4-2)", "input=sqrt(2)"), + Iteration("2*(6+3+4) + 4==2*(2+6+3+4)", "answer=2*(6+3+4) + 4", "input=2*(2+6+3+4)"), + Iteration("2*(2+6+3) + 8==2*(2+6+3+4)", "answer=2*(2+6+3) + 8", "input=2*(2+6+3+4)"), + Iteration("(2+6+3+4)*2==2*(2+6+3+4)", "answer=(2+6+3+4)*2", "input=2*(2+6+3+4)"), + Iteration("(2+6+3+4) × 2==2*(2+6+3+4)", "answer=(2+6+3+4) × 2", "input=2*(2+6+3+4)"), + Iteration("15 - 12 + 3==15 - (6 × 2) + 3", "answer=15 - 12 + 3", "input=15 - (6 × 2) + 3"), + Iteration( + "3 - (6 * 2) + 15==15 - (6 × 2) + 3", "answer=3 - (6 * 2) + 15", "input=15 - (6 × 2) + 3" + ), + Iteration( + "15 - (2 × 6) + 3==15 - (6 × 2) + 3", "answer=15 - (2 × 6) + 3", "input=15 - (6 × 2) + 3" + ), + Iteration( + "2 *(50 + 150) + 2*(100 + 25)==(50 + 150 + 100 + 25) × 2", + "answer=2 *(50 + 150) + 2*(100 + 25)", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration( + "2* ( 25+50+100+150)==(50 + 150 + 100 + 25) × 2", + "answer=2* ( 25+50+100+150)", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration("10^−5 * 3==3 * 10^-5", "answer=10^−5 * 3", "input=3 * 10^-5"), + Iteration("30 * 10^−6==3 * 10^-5", "answer=30 * 10^−6", "input=3 * 10^-5"), + Iteration("0.00003==3 * 10^-5", "answer=0.00003", "input=3 * 10^-5"), + Iteration("3/10^5==3 * 10^-5", "answer=3/10^5", "input=3 * 10^-5"), Iteration("3 *2 – (− 4)==6 − (− 4)", "answer=3 *2 – (− 4)", "input=6 − (− 4)"), -// Iteration("7==5+2", "answer=7", "input=5+2"), -// Iteration("3+4==5+2", "answer=3+4", "input=5+2") + Iteration("7==5+2", "answer=7", "input=5+2"), + Iteration("3+4==5+2", "answer=3+4", "input=5+2") ) fun testMatches_assortedExpressions_withMatchingCharacteristics_returnsTrue() { val answerExpression = createMathExpression(answer) diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt index 42b4c7e2b99..a64d6296159 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt @@ -20,6 +20,8 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule @@ -32,6 +34,7 @@ import org.robolectric.annotation.LooperMode // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest { diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt index bbd4085d420..e0aaeb73822 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt @@ -20,6 +20,8 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule @@ -32,6 +34,7 @@ import org.robolectric.annotation.LooperMode // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest { diff --git a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt index 3d8c27dfd37..b5362e166a7 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt @@ -9,8 +9,7 @@ import kotlin.math.abs * * Note that the machine epsilon value from https://en.wikipedia.org/wiki/Machine_epsilon is defined * defined as the smallest value that, when added to, or subtract from, 1, will result in a value - * that is exactly equal to 1. A slightly larger value is picked here for some allowance in - * variance. + * that is exactly equal to 1. A larger value is picked here for more allowance in variance. */ const val FLOAT_EQUALITY_EPSILON: Float = 1e-6f @@ -19,7 +18,7 @@ const val FLOAT_EQUALITY_EPSILON: Float = 1e-6f * * See [FLOAT_EQUALITY_EPSILON] for an explanation of this value. */ -const val DOUBLE_EQUALITY_EPSILON: Double = 1e-15 +const val DOUBLE_EQUALITY_EPSILON: Double = 1e-13 /** * Returns whether this float approximately equals another based on a consistent epsilon value 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 index 8e3b36f2dda..7a59fa5612c 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -198,7 +198,7 @@ oppia_android_test( "//model/src/main/proto:math_java_proto_lite", "//testing", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", - "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_junit_test_runner", "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", "//testing/src/main/java/org/oppia/android/testing/math:real_subject", "//third_party:com_google_truth_truth", diff --git a/utility/src/test/java/org/oppia/android/util/math/ComparableOperationExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/ComparableOperationExtensionsTest.kt index 0e277196fa3..719e30c8a8c 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ComparableOperationExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ComparableOperationExtensionsTest.kt @@ -129,7 +129,7 @@ class ComparableOperationExtensionsTest { @Test fun testIsApproximatelyEqualTo_firstIsConstInt2_secondIsConstDouble2PlusMargin_returnsTrue() { val first = createConstantOp(constant = 2) - val second = createConstantOp(constant = 2.0000001) + val second = createConstantOp(constant = 2.000000000000001) val result1 = first.isApproximatelyEqualTo(second) val result2 = second.isApproximatelyEqualTo(first) diff --git a/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt index 9b40f04c808..e814d753cae 100644 --- a/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt @@ -12,9 +12,8 @@ import org.robolectric.annotation.LooperMode @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class FloatExtensionsTest { - @Test - fun testFloat_approximatelyEquals_bothZero_returnsTrue() { + fun testFloat_isApproximatelyEqualTo_bothZero_returnsTrue() { val leftFloat = 0f val rightFloat = 0f @@ -24,7 +23,7 @@ class FloatExtensionsTest { } @Test - fun testFloat_approximatelyEquals_sameNonZeroValue_returnsTrue() { + fun testFloat_isApproximatelyEqualTo_sameNonZeroValue_returnsTrue() { val leftFloat = 1.2f val rightFloat = 1.2f @@ -34,7 +33,7 @@ class FloatExtensionsTest { } @Test - fun testFloat_approximatelyEquals_nonZeroValues_withinInterval_returnsTrue() { + fun testFloat_isApproximatelyEqualTo_nonZeroValues_withinInterval_returnsTrue() { val leftFloat = 1.2f val rightFloat = leftFloat + FLOAT_EQUALITY_EPSILON / 10f @@ -46,7 +45,7 @@ class FloatExtensionsTest { } @Test - fun testFloat_approximatelyEquals_zeroAndNonZeroValue_veryDifferent_returnsFalse() { + fun testFloat_isApproximatelyEqualTo_zeroAndNonZeroValue_veryDifferent_returnsFalse() { val leftFloat = 0f val rightFloat = 7.3f @@ -56,7 +55,7 @@ class FloatExtensionsTest { } @Test - fun testFloat_approximatelyEquals_nonZeroValues_outsideInterval_returnsFalse() { + fun testFloat_isApproximatelyEqualTo_nonZeroValues_outsideInterval_returnsFalse() { val leftFloat = 1.2f val rightFloat = leftFloat + FLOAT_EQUALITY_EPSILON * 2f @@ -66,7 +65,7 @@ class FloatExtensionsTest { } @Test - fun testFloat_approximatelyEquals_nonZeroValues_veryDifferent_returnsFalse() { + fun testFloat_isApproximatelyEqualTo_nonZeroValues_veryDifferent_returnsFalse() { val leftFloat = 1.2f val rightFloat = 7.3f @@ -76,7 +75,7 @@ class FloatExtensionsTest { } @Test - fun testDouble_approximatelyEquals_bothZero_returnsTrue() { + fun testDouble_isApproximatelyEqualTo_bothZero_returnsTrue() { val leftDouble = 0.0 val rightDouble = 0.0 @@ -86,7 +85,7 @@ class FloatExtensionsTest { } @Test - fun testDouble_approximatelyEquals_sameNonZeroValue_returnsTrue() { + fun testDouble_isApproximatelyEqualTo_sameNonZeroValue_returnsTrue() { val leftDouble = 1.2 val rightDouble = 1.2 @@ -96,7 +95,7 @@ class FloatExtensionsTest { } @Test - fun testDouble_approximatelyEquals_nonZeroValues_withinInterval_returnsTrue() { + fun testDouble_isApproximatelyEqualTo_nonZeroValues_withinInterval_returnsTrue() { val leftDouble = 0.2 val rightDouble = leftDouble + DOUBLE_EQUALITY_EPSILON / 10.0 @@ -108,7 +107,7 @@ class FloatExtensionsTest { } @Test - fun testDouble_approximatelyEquals_nonZeroValues_outsideInterval_returnsFalse() { + fun testDouble_isApproximatelyEqualTo_nonZeroValues_outsideInterval_returnsFalse() { val leftDouble = 1.2 val rightDouble = leftDouble + DOUBLE_EQUALITY_EPSILON * 2 @@ -118,7 +117,7 @@ class FloatExtensionsTest { } @Test - fun testDouble_approximatelyEquals_nonZeroValues_veryDifferent_returnsFalse() { + fun testDouble_isApproximatelyEqualTo_nonZeroValues_veryDifferent_returnsFalse() { val leftDouble = 1.2 val rightDouble = 7.3 diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt index abc2888f94e..a973125994e 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt @@ -1,16 +1,16 @@ package org.oppia.android.util.math import com.google.common.truth.Truth.assertThat -import java.lang.IllegalStateException import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression -import org.oppia.android.testing.assertThrows import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedJunitTestRunner import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode @@ -35,6 +35,7 @@ import org.robolectric.annotation.LooperMode // SameParameterValue: tests should have specific context included/excluded for readability. @Suppress("FunctionName", "SameParameterValue") @RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedJunitTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class MathExpressionExtensionsTest { @Parameter lateinit var exp1: String @@ -173,7 +174,7 @@ class MathExpressionExtensionsTest { @Test @RunParameterized( Iteration("2==2", "exp1=2", "exp2=2"), - Iteration("2==2.000000001", "exp1=2", "exp2=2.000000001"), + Iteration("2==2.000000000000001", "exp1=2", "exp2=2.000000000000001"), Iteration("x+1==x+1", "exp1=x+1", "exp2=x+1"), Iteration("x-1==x-1", "exp1=x-1", "exp2=x-1"), Iteration("x*2==x*2", "exp1=x*2", "exp2=x*2"), diff --git a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt index 28b91fcbce3..d8a25a368db 100644 --- a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt @@ -1379,7 +1379,7 @@ class PolynomialExtensionsTest { val first = createPolynomial(createTerm(coefficient = TWO_REAL)) val second = createPolynomial( createTerm(coefficient = Real.newBuilder().apply { - irrational = 2.00000001 + irrational = 2.00000000000000001 }.build()) ) @@ -1420,7 +1420,7 @@ class PolynomialExtensionsTest { fun testIsApproximatelyEqualTo_firstIsDoublePointThrees_secondIsFracOneThird_returnsTrue() { val first = createPolynomial( createTerm(coefficient = Real.newBuilder().apply { - irrational = 0.33333333333 + irrational = 0.33333333333333333 }.build()) ) val second = createPolynomial(createTerm(coefficient = ONE_THIRD_REAL)) diff --git a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt index a2133f5c59f..fee8d4e00e7 100644 --- a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt @@ -410,11 +410,11 @@ class RealExtensionsTest { Iteration("0==0.0", "lhsInt=0", "rhsDouble=0.0"), Iteration("1==1.0", "lhsInt=1", "rhsDouble=1.0"), Iteration("2==2.0", "lhsInt=2", "rhsDouble=2.0"), - Iteration("2==2.0000001", "lhsInt=2", "rhsDouble=2.0000001"), - Iteration("2==1.9999999", "lhsInt=2", "rhsDouble=1.9999999"), + Iteration("2==2.000000000000001", "lhsInt=2", "rhsDouble=2.000000000000001"), + Iteration("2==1.999999999999999", "lhsInt=2", "rhsDouble=1.999999999999999"), Iteration("-2==-2.0", "lhsInt=-2", "rhsDouble=-2.0"), - Iteration("-2==-2.0000001", "lhsInt=-2", "rhsDouble=-2.0000001"), - Iteration("-2==-1.9999999", "lhsInt=-2", "rhsDouble=-1.9999999") + Iteration("-2==-2.00000000000001", "lhsInt=-2", "rhsDouble=-2.00000000000001"), + Iteration("-2==-1.999999999999999", "lhsInt=-2", "rhsDouble=-1.999999999999999") ) fun testIsApproximatelyEqualTo_oneIsInt_otherIsSimilarDouble_returnsTrue() { val first = createIntegerReal(lhsInt) @@ -502,8 +502,8 @@ class RealExtensionsTest { Iteration("2==2.0", "lhsFrac=2", "rhsDouble=2.0"), Iteration("2/1==2.0", "lhsFrac=2/1", "rhsDouble=2.0"), Iteration("3/2==1.5", "lhsFrac=3/2", "rhsDouble=1.5"), - Iteration("1/3==0.33333333333", "lhsFrac=1/3", "rhsDouble=0.33333333333"), - Iteration("1 2/3==1.66666666666", "lhsFrac=1 2/3", "rhsDouble=1.66666666666"), + Iteration("1/3==0.33333333333333333", "lhsFrac=1/3", "rhsDouble=0.33333333333333333"), + Iteration("1 2/3==1.66666666666666666", "lhsFrac=1 2/3", "rhsDouble=1.66666666666666666"), Iteration("-2==-2.0", "lhsFrac=-2", "rhsDouble=-2.0"), Iteration("-3/2==-1.5", "lhsFrac=-3/2", "rhsDouble=-1.5") ) @@ -548,10 +548,18 @@ class RealExtensionsTest { @RunParameterized( Iteration("0.0==0.0", "lhsDouble=0.0", "rhsDouble=0.0"), Iteration("2.0==2.0", "lhsDouble=2.0", "rhsDouble=2.0"), - Iteration("2.0000000001==1.9999999999", "lhsDouble=2.0000000001", "rhsDouble=1.9999999999"), + Iteration( + "2.000000000000001==1.999999999999999", + "lhsDouble=2.000000000000001", + "rhsDouble=1.999999999999999" + ), Iteration("3.14==3.14", "lhsDouble=3.14", "rhsDouble=3.14"), Iteration("-2.0==-2.0", "lhsDouble=-2.0", "rhsDouble=-2.0"), - Iteration("-2.0000000001==-1.9999999999", "lhsDouble=-2.0000000001", "rhsDouble=-1.9999999999"), + Iteration( + "-2.000000000000001==-1.999999999999999", + "lhsDouble=-2.000000000000001", + "rhsDouble=-1.999999999999999" + ), Iteration("-3.14==-3.14", "lhsDouble=-3.14", "rhsDouble=-3.14") ) fun testIsApproximatelyEqualTo_oneIsDouble_otherIsSimilarDouble_returnsTrue() { From 3248e84dd6d16ff2ad043ae9a5779cb407035b12 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 18:24:52 -0800 Subject: [PATCH 106/134] Lint & other check fixes. --- ...putIsEquivalentToRuleClassifierProvider.kt | 8 +++- ...atchesExactlyWithRuleClassifierProvider.kt | 9 +++- ...vialManipulationsRuleClassifierProvider.kt | 11 ++++- ...sEquivalentToRuleClassifierProviderTest.kt | 14 +++--- ...esExactlyWithRuleClassifierProviderTest.kt | 43 ++++++++++++------- ...ManipulationsRuleClassifierProviderTest.kt | 23 ++++++---- .../NumericExpressionInputModuleTest.kt | 4 +- .../file_content_validation_checks.textproto | 4 ++ .../math/ComparableOperationExtensions.kt | 4 +- .../util/math/MathExpressionExtensions.kt | 14 +++--- .../math/ComparableOperationExtensionsTest.kt | 5 ++- .../util/math/MathExpressionExtensionsTest.kt | 3 +- .../util/math/PolynomialExtensionsTest.kt | 16 ++++--- 13 files changed, 106 insertions(+), 52 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt index 5d041e3e2ce..92b22428778 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -1,6 +1,5 @@ package org.oppia.android.domain.classify.rules.numericexpressioninput -import javax.inject.Inject import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.Real import org.oppia.android.app.model.WrittenTranslationContext @@ -12,7 +11,14 @@ import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.evaluateAsNumericExpression import org.oppia.android.util.math.isApproximatelyEqualTo +import javax.inject.Inject +/** + * Provider for a classifier that determines whether a numeric expression is numerically equivalent + * to the creator-specific expression defined as the input to this interaction. + * + * See this class's tests for a list of supported cases (both for matching and not matching). + */ class NumericExpressionInputIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, private val consoleLogger: ConsoleLogger diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt index a62e75d9acb..653197f0727 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -9,9 +9,16 @@ import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult -import javax.inject.Inject import org.oppia.android.util.math.isApproximatelyEqualTo +import javax.inject.Inject +/** + * Provider for a classifier that determines whether a numeric expression is exactly equal to the + * creator-specific expression defined as the input to this interaction, including any parenthetical + * groups in the expressions and operand order. + * + * See this class's tests for a list of supported cases (both for matching and not matching). + */ class NumericExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, private val consoleLogger: ConsoleLogger diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index b4f4b074099..fe45d1cf6ba 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -9,10 +9,19 @@ import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.isApproximatelyEqualTo import org.oppia.android.util.math.toComparableOperation import javax.inject.Inject -import org.oppia.android.util.math.isApproximatelyEqualTo +/** + * Provider for a classifier that determines whether a numeric expression is equal to the + * creator-specific expression defined as the input to this interaction, with some manipulations. + * + * 'Trivial manipulations' indicates rearranging any operands for commutative operations, or changes + * in resolution order (i.e. associative) without changing the meaning of the expression. + * + * See this class's tests for a list of supported cases (both for matching and not matching). + */ class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt index 2d2aee241b2..b2f289f384c 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt @@ -8,8 +8,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import javax.inject.Inject -import javax.inject.Singleton import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -29,8 +27,16 @@ import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton -/** Tests for [NumericExpressionInputIsEquivalentToRuleClassifierProvider]. */ +/** + * Tests for [NumericExpressionInputIsEquivalentToRuleClassifierProvider]. + * + * Note that the tests implemented in this suite are specifically set up to verify the cases + * outlined in this sheet: + * https://docs.google.com/spreadsheets/d/1u1fQdah2WsmdYKWKGmuXy5TPT7Ot-b8A7O9iZF-j5XE/edit#gid=0. + */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @RunWith(OppiaParameterizedTestRunner::class) @@ -38,8 +44,6 @@ import org.robolectric.annotation.LooperMode @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class NumericExpressionInputIsEquivalentToRuleClassifierProviderTest { - // TODO: add details about the sheet to this test's KDoc. - @Inject internal lateinit var provider: NumericExpressionInputIsEquivalentToRuleClassifierProvider diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt index a64d6296159..ec362363cb8 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt @@ -8,8 +8,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import javax.inject.Inject -import javax.inject.Singleton import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -29,8 +27,17 @@ import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode - -/** Tests for [NumericExpressionInputMatchesExactlyWithRuleClassifierProvider]. */ +import javax.inject.Inject +import javax.inject.Singleton +import org.oppia.android.domain.classify.rules.numericexpressioninput.DaggerNumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest_TestApplicationComponent as DaggerTestApplicationComponent + +/** + * Tests for [NumericExpressionInputMatchesExactlyWithRuleClassifierProvider]. + * + * Note that the tests implemented in this suite are specifically set up to verify the cases + * outlined in this sheet: + * https://docs.google.com/spreadsheets/d/1u1fQdah2WsmdYKWKGmuXy5TPT7Ot-b8A7O9iZF-j5XE/edit#gid=0. + */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @RunWith(OppiaParameterizedTestRunner::class) @@ -38,8 +45,6 @@ import org.robolectric.annotation.LooperMode @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest { - // TODO: add details about the sheet to this test's KDoc. - @Inject internal lateinit var provider: NumericExpressionInputMatchesExactlyWithRuleClassifierProvider @@ -245,27 +250,33 @@ class NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest { Iteration( "0.06 + 0.5 + 4 + 30 + 200 + 1000!=1000 + 200 + 30 + 4 + 0.5 + 0.06", "answer=0.06 + 0.5 + 4 + 30 + 200 + 1000", - "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), Iteration( "1234.56!=1000 + 200 + 30 + 4 + 0.5 + 0.06", "answer=1234.56", - "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), Iteration( "123456/100!=1000 + 200 + 30 + 4 + 0.5 + 0.06", "answer=123456/100", - "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), Iteration( "61728/50!=1000 + 200 + 30 + 4 + 0.5 + 0.06", "answer=61728/50", - "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), Iteration( "1234 + 56/10!=1000 + 200 + 30 + 4 + 0.5 + 0.06", "answer=1234 + 56/10", - "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), Iteration( "1230 + 4.56!=1000 + 200 + 30 + 4 + 0.5 + 0.06", "answer=1230 + 4.56", - "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), Iteration( "2 * 2 * 3 * 3 * 1!=2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3 * 1", "input=2 * 2 * 3 * 3" ), @@ -305,11 +316,13 @@ class NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest { Iteration( "123456!=1000 + 200 + 30 + 4 + 0.5 + 0.06", "answer=123456", - "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), Iteration( "1000 + 200 + 30!=1000 + 200 + 30 + 4 + 0.5 + 0.06", "answer=1000 + 200 + 30", - "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), Iteration("3 *2 – (− 4)!=6 − (− 4)", "answer=3 *2 – (− 4)", "input=6 − (− 4)"), Iteration("6 − 4!=6 − (− 4)", "answer=6 − 4", "input=6 − (− 4)"), Iteration("6 + (− 4)!=6 − (− 4)", "answer=6 + (− 4)", "input=6 − (− 4)"), @@ -346,7 +359,7 @@ class NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest { }.build() private fun setUpTestApplicationComponent() { - DaggerNumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest_TestApplicationComponent + DaggerTestApplicationComponent .builder() .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt index e0aaeb73822..f75c05b451a 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt @@ -8,8 +8,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import javax.inject.Inject -import javax.inject.Singleton import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -29,8 +27,18 @@ import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode - -/** Tests for [NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider]. */ +import javax.inject.Inject +import javax.inject.Singleton +import org.oppia.android.domain.classify.rules.numericexpressioninput.DaggerNumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent as DaggerTestApplicationComponent +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider as RuleClassifierProvider + +/** + * Tests for [RuleClassifierProvider]. + * + * Note that the tests implemented in this suite are specifically set up to verify the cases + * outlined in this sheet: + * https://docs.google.com/spreadsheets/dNumericExpressionInputIsEquivalentToRuleClassifierProvider/1u1fQdah2WsmdYKWKGmuXy5TPT7Ot-b8A7O9iZF-j5XE/edit#gid=0. + */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @RunWith(OppiaParameterizedTestRunner::class) @@ -38,10 +46,7 @@ import org.robolectric.annotation.LooperMode @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest { - // TODO: add details about the sheet to this test's KDoc. - - @Inject - internal lateinit var provider: NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider + @Inject internal lateinit var provider: RuleClassifierProvider @Parameter lateinit var answer: String @Parameter lateinit var input: String @@ -368,7 +373,7 @@ class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvide }.build() private fun setUpTestApplicationComponent() { - DaggerNumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent + DaggerTestApplicationComponent .builder() .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModuleTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModuleTest.kt index 3ad0eed220e..b12cc16af95 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModuleTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModuleTest.kt @@ -9,8 +9,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import javax.inject.Inject -import javax.inject.Singleton import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -23,6 +21,8 @@ import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton /** Tests for [NumericExpressionInputModule]. */ // FunctionName: test names are conventionally named with underscores. diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index 0c9432585fb..1408521c618 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -286,8 +286,12 @@ file_content_checks { file_path_regex: ".+?\\.kt" prohibited_content_regex: "OppiaParameterizedTestRunner" failure_message: "To use OppiaParameterizedTestRunner, please add an exemption to file_content_validation_checks.textproto and add an explanation for your use case in your PR description. Note that parameterized tests should only be used in special circumstances where a single behavior can be tested across multiple inputs, or for especially large test suites that can be trivially reduced." + exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt" + exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt" + exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt" exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt" + exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt" diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparableOperationExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparableOperationExtensions.kt index 249bbea8394..65dadeebecc 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ComparableOperationExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ComparableOperationExtensions.kt @@ -53,8 +53,8 @@ private fun NonCommutativeOperation.isApproximatelyEqualTo( if (operationTypeCase != other.operationTypeCase) return false return when (operationTypeCase) { EXPONENTIATION -> { - exponentiation.leftOperand.isApproximatelyEqualTo(other.exponentiation.leftOperand) - && exponentiation.rightOperand.isApproximatelyEqualTo(other.exponentiation.rightOperand) + exponentiation.leftOperand.isApproximatelyEqualTo(other.exponentiation.leftOperand) && + exponentiation.rightOperand.isApproximatelyEqualTo(other.exponentiation.rightOperand) } SQUARE_ROOT -> squareRoot.isApproximatelyEqualTo(other.squareRoot) OPERATIONTYPE_NOT_SET, null -> true diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index ab2cdd1d5c2..165c6a1cf26 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -66,17 +66,17 @@ fun MathExpression.isApproximatelyEqualTo(other: MathExpression): Boolean { CONSTANT -> constant.isApproximatelyEqualTo(other.constant) VARIABLE -> variable == other.variable BINARY_OPERATION -> { - binaryOperation.operator == other.binaryOperation.operator - && binaryOperation.leftOperand.isApproximatelyEqualTo(other.binaryOperation.leftOperand) - && binaryOperation.rightOperand.isApproximatelyEqualTo(other.binaryOperation.rightOperand) + binaryOperation.operator == other.binaryOperation.operator && + binaryOperation.leftOperand.isApproximatelyEqualTo(other.binaryOperation.leftOperand) && + binaryOperation.rightOperand.isApproximatelyEqualTo(other.binaryOperation.rightOperand) } UNARY_OPERATION -> { - unaryOperation.operator == other.unaryOperation.operator - && unaryOperation.operand.isApproximatelyEqualTo(other.unaryOperation.operand) + unaryOperation.operator == other.unaryOperation.operator && + unaryOperation.operand.isApproximatelyEqualTo(other.unaryOperation.operand) } FUNCTION_CALL -> { - functionCall.functionType == other.functionCall.functionType - && functionCall.argument.isApproximatelyEqualTo(other.functionCall.argument) + functionCall.functionType == other.functionCall.functionType && + functionCall.argument.isApproximatelyEqualTo(other.functionCall.argument) } GROUP -> group.isApproximatelyEqualTo(other.group) EXPRESSIONTYPE_NOT_SET, null -> true diff --git a/utility/src/test/java/org/oppia/android/util/math/ComparableOperationExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/ComparableOperationExtensionsTest.kt index 719e30c8a8c..f7198f2ba4e 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ComparableOperationExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ComparableOperationExtensionsTest.kt @@ -738,7 +738,8 @@ class ComparableOperationExtensionsTest { }.build() private fun createExpOp( - lhs: ComparableOperation, rhs: ComparableOperation + lhs: ComparableOperation, + rhs: ComparableOperation ) = ComparableOperation.newBuilder().apply { nonCommutativeOperation = NonCommutativeOperation.newBuilder().apply { exponentiation = NonCommutativeOperation.BinaryOperation.newBuilder().apply { @@ -759,7 +760,7 @@ class ComparableOperationExtensionsTest { private fun createIntegerReal(value: Int) = Real.newBuilder().apply { integer = value }.build() - + private fun createRationalReal(rawFractionExpression: String) = Real.newBuilder().apply { rational = fractionParser.parseFractionFromString(rawFractionExpression) }.build() diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt index a973125994e..6406f3e6e70 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt @@ -282,7 +282,8 @@ class MathExpressionExtensionsTest { } private fun parseAlgebraicExpression( - expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + expression: String, + errorCheckingMode: ErrorCheckingMode = ALL_ERRORS ): MathExpression { return parseAlgebraicExpression( expression, allowedVariables = listOf("x", "y", "z"), errorCheckingMode diff --git a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt index d8a25a368db..b3cc704c4c9 100644 --- a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt @@ -1378,9 +1378,11 @@ class PolynomialExtensionsTest { fun testIsApproximatelyEqualTo_firstIsPolyOfInt2_secondIsPolyOfDouble2PlusMargin_returnsTrue() { val first = createPolynomial(createTerm(coefficient = TWO_REAL)) val second = createPolynomial( - createTerm(coefficient = Real.newBuilder().apply { - irrational = 2.00000000000000001 - }.build()) + createTerm( + coefficient = Real.newBuilder().apply { + irrational = 2.00000000000000001 + }.build() + ) ) val result1 = first.isApproximatelyEqualTo(second) @@ -1419,9 +1421,11 @@ class PolynomialExtensionsTest { @Test fun testIsApproximatelyEqualTo_firstIsDoublePointThrees_secondIsFracOneThird_returnsTrue() { val first = createPolynomial( - createTerm(coefficient = Real.newBuilder().apply { - irrational = 0.33333333333333333 - }.build()) + createTerm( + coefficient = Real.newBuilder().apply { + irrational = 0.33333333333333333 + }.build() + ) ) val second = createPolynomial(createTerm(coefficient = ONE_THIRD_REAL)) From 3960220a08fd3b7eb6af2dcfebbbf00203ee467d Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 18:38:50 -0800 Subject: [PATCH 107/134] Replace deprecated term. --- ...MatchesUpToTrivialManipulationsRuleClassifierProvider.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index fe45d1cf6ba..b616874d193 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -40,12 +40,12 @@ class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvide input: String, writtenTranslationContext: WrittenTranslationContext ): Boolean { - val answerExpression = parseComparableOperationList(answer) ?: return false - val inputExpression = parseComparableOperationList(input) ?: return false + val answerExpression = parseComparableOperation(answer) ?: return false + val inputExpression = parseComparableOperation(input) ?: return false return answerExpression.isApproximatelyEqualTo(inputExpression) } - private fun parseComparableOperationList(rawExpression: String): ComparableOperation? { + private fun parseComparableOperation(rawExpression: String): ComparableOperation? { return when (val expResult = MathExpressionParser.parseNumericExpression(rawExpression)) { is MathParsingResult.Success -> expResult.result.toComparableOperation() is MathParsingResult.Failure -> { From f7ec7511db78466c3677495b081aa645a82d5201 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 20:15:48 -0800 Subject: [PATCH 108/134] Post-merge fixes. --- ...putIsEquivalentToRuleClassifierProvider.kt | 4 ++-- ...atchesExactlyWithRuleClassifierProvider.kt | 4 ++-- ...vialManipulationsRuleClassifierProvider.kt | 20 +++++++++---------- ...sEquivalentToRuleClassifierProviderTest.kt | 3 ++- ...esExactlyWithRuleClassifierProviderTest.kt | 3 ++- ...ManipulationsRuleClassifierProviderTest.kt | 3 ++- 6 files changed, 20 insertions(+), 17 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt index a6ad8e8e010..56e771b93c0 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -11,7 +11,7 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingRes import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression import org.oppia.android.util.math.toPolynomial import javax.inject.Inject -import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo class AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -33,7 +33,7 @@ class AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider @Inject const val allowedVariables = classificationContext.extractAllowedVariables() val answerExpression = parsePolynomial(answer, allowedVariables) ?: return false val inputExpression = parsePolynomial(input, allowedVariables) ?: return false - return answerExpression.approximatelyEquals(inputExpression) + return answerExpression.isApproximatelyEqualTo(inputExpression) } private fun parsePolynomial(rawExpression: String, allowedVariables: List): Polynomial? { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt index 6feeba0696a..4c20ce52903 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -10,7 +10,7 @@ import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression import javax.inject.Inject -import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -32,7 +32,7 @@ class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject c val allowedVariables = classificationContext.extractAllowedVariables() val answerExpression = parseExpression(answer, allowedVariables) ?: return false val inputExpression = parseExpression(input, allowedVariables) ?: return false - return answerExpression.approximatelyEquals(inputExpression) + return answerExpression.isApproximatelyEqualTo(inputExpression) } private fun parseExpression( diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index 03c39a3c64f..6f618dfc857 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -1,6 +1,7 @@ package org.oppia.android.domain.classify.rules.algebraicexpressioninput -import org.oppia.android.app.model.ComparableOperationList +import javax.inject.Inject +import org.oppia.android.app.model.ComparableOperation import org.oppia.android.app.model.InteractionObject import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier @@ -9,9 +10,8 @@ import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression -import org.oppia.android.util.math.toComparableOperationList -import javax.inject.Inject -import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo +import org.oppia.android.util.math.toComparableOperation class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider @Inject constructor( @@ -32,17 +32,17 @@ class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvi classificationContext: ClassificationContext ): Boolean { val allowedVariables = classificationContext.extractAllowedVariables() - val answerExpression = parseComparableOperationList(answer, allowedVariables) ?: return false - val inputExpression = parseComparableOperationList(input, allowedVariables) ?: return false - return answerExpression.approximatelyEquals(inputExpression) + val answerExpression = parseComparableOperation(answer, allowedVariables) ?: return false + val inputExpression = parseComparableOperation(input, allowedVariables) ?: return false + return answerExpression.isApproximatelyEqualTo(inputExpression) } - private fun parseComparableOperationList( + private fun parseComparableOperation( rawExpression: String, allowedVariables: List - ): ComparableOperationList? { + ): ComparableOperation? { return when (val expResult = parseAlgebraicExpression(rawExpression, allowedVariables)) { - is MathParsingResult.Success -> expResult.result.toComparableOperationList() + is MathParsingResult.Success -> expResult.result.toComparableOperation() is MathParsingResult.Failure -> { consoleLogger.e( "AlgebraExpTrivialManips", diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt index b2f289f384c..4ef5e2928c4 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt @@ -29,6 +29,7 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton +import org.oppia.android.domain.classify.ClassificationContext /** * Tests for [NumericExpressionInputIsEquivalentToRuleClassifierProvider]. @@ -349,7 +350,7 @@ class NumericExpressionInputIsEquivalentToRuleClassifierProviderTest { return classifier.matches( answerExpression, inputs = mapOf("x" to inputExpression), - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt index ec362363cb8..228e1e3582e 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt @@ -29,6 +29,7 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.rules.numericexpressioninput.DaggerNumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest_TestApplicationComponent as DaggerTestApplicationComponent /** @@ -350,7 +351,7 @@ class NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest { return classifier.matches( answerExpression, inputs = mapOf("x" to inputExpression), - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt index f75c05b451a..beef3fdf622 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt @@ -29,6 +29,7 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.rules.numericexpressioninput.DaggerNumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent as DaggerTestApplicationComponent import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider as RuleClassifierProvider @@ -364,7 +365,7 @@ class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvide return classifier.matches( answerExpression, inputs = mapOf("x" to inputExpression), - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } From 782e05829cbe76bfe84d28cee28320ee75f41b51 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 22:44:12 -0800 Subject: [PATCH 109/134] Add full test suites for alg exp classifiers. --- ...sEquivalentToRuleClassifierProviderTest.kt | 665 +++++++++++++++++ ...esExactlyWithRuleClassifierProviderTest.kt | 649 +++++++++++++++++ ...ManipulationsRuleClassifierProviderTest.kt | 666 ++++++++++++++++++ .../AlgebraicExpressionInputModuleTest.kt | 107 +++ .../algebraicexpressioninput/BUILD.bazel | 105 +++ ...ManipulationsRuleClassifierProviderTest.kt | 4 +- .../util/math/PolynomialExtensionsTest.kt | 2 +- 7 files changed, 2195 insertions(+), 3 deletions(-) create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModuleTest.kt create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/BUILD.bazel diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt new file mode 100644 index 00000000000..60d09fbf36b --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt @@ -0,0 +1,665 @@ +package org.oppia.android.domain.classify.rules.algebraicexpressioninput + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner +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.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton +import org.oppia.android.app.model.SchemaObject +import org.oppia.android.app.model.SchemaObjectList +import org.oppia.android.domain.classify.ClassificationContext + +/** + * Tests for [AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider]. + * + * Note that the tests implemented in this suite are specifically set up to verify the cases + * outlined in this sheet: + * https://docs.google.com/spreadsheets/d/1u1fQdah2WsmdYKWKGmuXy5TPT7Ot-b8A7O9iZF-j5XE/edit#gid=0. + */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest { + @Inject + internal lateinit var provider: AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider + + @Parameter lateinit var answer: String + @Parameter lateinit var input: String + + private lateinit var classifier: RuleClassifier + private val allPossibleVariables = (('a'..'z') + ('A'..'Z')).toList().map { it.toString() } + + @Before + fun setUp() { + setUpTestApplicationComponent() + classifier = provider.createRuleClassifier() + } + + @Test + fun testMatches_answerHasDisallowedVariable_returnsFalse() { + val answerExpression = createMathExpression("y") + val inputExpression = createMathExpression("y") + + val matches = matchesClassifier(answerExpression, inputExpression, allowedVariables = listOf()) + + // Despite the answer otherwise being equal, the variable isn't allowed. This shouldn't actually + // be the case in practice since neither the creator nor the learner would be allowed to input a + // disallowed variable (so this check is mainly a "just-in-case"). + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("0==0", "answer=0", "input=0"), + Iteration("1==1", "answer=1", "input=1"), + Iteration("2==2", "answer=2", "input=2"), + Iteration("x==x", "answer=x", "input=x"), + Iteration("y==y", "answer=y", "input=y") + ) + fun testMatches_sameSingleTerms_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("-2==-2", "answer=-2", "input=-2"), + Iteration("1+3.14==1+3.14", "answer=1+3.14", "input=1+3.14"), + Iteration(" 1 + 3.14 ==1+3.14", "answer= 1 + 3.14 ", "input=1+3.14"), + Iteration("1+2+3==1+2+3", "answer=1+2+3", "input=1+2+3"), + Iteration("1-3.14==1-3.14", "answer=1-3.14", "input=1-3.14"), + Iteration("2*3.14==2*3.14", "answer=2*3.14", "input=2*3.14"), + Iteration("2/3==2/3", "answer=2/3", "input=2/3"), + Iteration("2/3.14==2/3.14", "answer=2/3.14", "input=2/3.14"), + Iteration("2^3==2^3", "answer=2^3", "input=2^3"), + Iteration("2^3.14==2^3.14", "answer=2^3.14", "input=2^3.14"), + Iteration("sqrt(2)==sqrt(2)", "answer=sqrt(2)", "input=sqrt(2)"), + Iteration("-x==-x", "answer=-x", "input=-x"), + Iteration("x+3.14==x+3.14", "answer=x+3.14", "input=x+3.14"), + Iteration("x-3.14==x-3.14", "answer=x-3.14", "input=x-3.14"), + Iteration("x*3.14==x*3.14", "answer=x*3.14", "input=x*3.14"), + Iteration("x/3==x/3", "answer=x/3", "input=x/3") + ) + fun testMatches_sameSingleOperations_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("sqrt(x)!=sqrt(x)", "answer=sqrt(x)", "input=sqrt(x)") + ) + fun testMatches_sameSingleOperations_thatCannotBecomePolynomials_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Despite the terms being the same, if they can't be converted to a polynomial then the + // classifier won't match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("1!=0", "answer=1", "input=0"), + Iteration("0!=1", "answer=0", "input=1"), + Iteration("3.14!=1", "answer=3.14", "input=1"), + Iteration("1!=3.14", "answer=1", "input=3.14"), + Iteration("x!=3.14", "answer=x", "input=3.14"), + Iteration("y!=x", "answer=y", "input=x"), + Iteration("3.14!=x", "answer=3.14", "input=x") + ) + fun testMatches_differentSingleTerms_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions don't evaluate to the same value then the classifier won't match them. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("3.14+1==1+3.14", "answer=3.14+1", "input=1+3.14"), + Iteration("3+2+1==1+2+3", "answer=3+2+1", "input=1+2+3"), + Iteration("-3.14+1==1-3.14", "answer=-3.14+1", "input=1-3.14"), + Iteration("3.14*2==2*3.14", "answer=3.14*2", "input=2*3.14"), + Iteration("2+x==x+2", "answer=2+x", "input=x+2"), + Iteration("y+x==x+y", "answer=y+x", "input=x+y"), + Iteration("x*2==2x", "answer=x*2", "input=2x"), + Iteration("yx==xy", "answer=yx", "input=xy") + ) + fun testMatches_operationsDiffer_byCommutativity_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Reordering terms by commutativity is allowed by this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("1+(2+3)==(1+2)+3", "answer=1+(2+3)", "input=(1+2)+3"), + Iteration("2*(3*4)==(2*3)*4", "answer=2*(3*4)", "input=(2*3)*4"), + Iteration("x+(2+3)==(x+2)+3", "answer=x+(2+3)", "input=(x+2)+3"), + Iteration("x+(y+z)==(x+y)+z", "answer=x+(y+z)", "input=(x+y)+z"), + Iteration("2*(3x)==(2x)*3", "answer=2*(3x)", "input=(2x)*3"), + Iteration("x(yz)==(xy)z", "answer=x(yz)", "input=(xy)z") + ) + fun testMatches_operationsDiffer_byAssociativity_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Changing operation associativity is allowed by this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("3.14-1!=1-3.14", "answer=3.14-1", "input=1-3.14"), + Iteration("1-(2-3)!=(1-2)-3", "answer=1-(2-3)", "input=(1-2)-3"), + Iteration("3.14/2!=2/3.14", "answer=3.14/2", "input=2/3.14"), + Iteration("2/(3/4)!=(2/3)/4", "answer=2/(3/4)", "input=(2/3)/4"), + Iteration("3.14^2!=2^3.14", "answer=3.14^2", "input=2^3.14"), + Iteration("3.14-x!=x-3.14", "answer=3.14-x", "input=x-3.14"), + Iteration("x-(y-z)!=(x-y)-z", "answer=x-(y-z)", "input=(x-y)-z"), + Iteration("3.14/x!=x/3.14", "answer=3.14/x", "input=x/3.14"), + Iteration("x/(y/z)!=(x/y)/z", "answer=x/(y/z)", "input=(x/y)/z") + ) + fun testMatches_operationsDiffer_byNonCommutativeOrAssociativeReordering_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Non-commutative and non-associative reordering generally results in a different value, so the + // classifier will fail to match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("1-2==-(2-1)", "answer=1-2", "input=-(2-1)"), + Iteration("1+2==1+1+1", "answer=1+2", "input=1+1+1"), + Iteration("1+2==1-(-2)", "answer=1+2", "input=1-(-2)"), + Iteration("4-6==1-2-1", "answer=4-6", "input=1-2-1"), + Iteration("2*3*2*2==2*3*4", "answer=2*3*2*2", "input=2*3*4"), + Iteration("-6-2==2*-(3+1)", "answer=-6-2", "input=2*-(3+1)"), + Iteration("2/3/2/2==2/3/4", "answer=2/3/2/2", "input=2/3/4"), + Iteration("2^(2+1)==2^3", "answer=2^(2+1)", "input=2^3"), + Iteration("2^(-1)==1/2", "answer=2^(-1)", "input=1/2"), + Iteration("x-y==-(y-x)", "answer=x-y", "input=-(y-x)"), + Iteration("2+x==1+x+1", "answer=2+x", "input=1+x+1"), + Iteration("1+x==1-(-x)", "answer=1+x", "input=1-(-x)"), + Iteration("-x==1-x-1", "answer=-x", "input=1-x-1"), + Iteration("4x==2*2*x", "answer=4x", "input=2*2*x"), + Iteration("2-6x==2*(-3x+1)", "answer=2-6x", "input=2*(-3x+1)"), + Iteration("x/4==x/2/2", "answer=x/4", "input=x/2/2"), + Iteration("x^(2+1)==x^3", "answer=x^(2+1)", "input=x^3"), + Iteration("x*(2^(-1))==x/2", "answer=x*(2^(-1))", "input=x/2") + ) + fun testMatches_operationsDiffer_byDistributionAndCombining_returnTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier supports any distribution or combining of terms. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("3x(3x - 2) + 1==(3x-1)^2", "answer=3x(3x - 2) + 1", "input=(3x-1)^2"), + Iteration("3(3x^2) - 6x +1==(3x-1)^2", "answer=3(3x^2) - 6x +1", "input=(3x-1)^2"), + Iteration("(1 - 3x)^2!=(3x-1)^2", "answer=(1 - 3x)^2", "input=(3x-1)^2"), + Iteration("2x==sqrt(4x^2)", "answer=2x", "input=sqrt(4x^2)"), + Iteration("x^2+2x+1==(x+1)^2", "answer=x^2+2x+1", "input=(x+1)^2"), + Iteration("x^2-1==(x+1)(x-1)", "answer=x^2-1", "input=(x+1)(x-1)"), + Iteration("x+1==(x^2+2x+1)/(x+1)", "answer=x+1", "input=(x^2+2x+1)/(x+1)"), + Iteration("x-1==(x^2-1)/(x+1)", "answer=x-1", "input=(x^2-1)/(x+1)"), + Iteration("x+1==(x^2-1)/(x-1)", "answer=x+1", "input=(x^2-1)/(x-1)"), + Iteration("-3x==(-27x^3)^(1/3)", "answer=-3x", "input=(-27x^3)^(1/3)"), + Iteration("1==(x^2-1)/(x^2-1)", "answer=1", "input=(x^2-1)/(x^2-1)"), + Iteration("2*(2+6+3+4)==2*(2+6+3+4)", "answer=2*(2+6+3+4)", "input=2*(2+6+3+4)"), + Iteration("2 × (2+6+3+4)==2*(2+6+3+4)", "answer=2 × (2+6+3+4)", "input=2*(2+6+3+4)"), + Iteration( + "15 - (6 × 2) + 3==15 - (6 × 2) + 3", "answer=15 - (6 × 2) + 3", "input=15 - (6 × 2) + 3" + ), + Iteration( + "2 × (50 + 150 + 100 + 25) ==(50 + 150 + 100 + 25) × 2", + "answer=2 × (50 + 150 + 100 + 25) ", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration( + "2 * (50 + 150 + 100 + 25) ==2 × (50 + 150 + 100 + 25)", + "answer=2 * (50 + 150 + 100 + 25) ", + "input=2 × (50 + 150 + 100 + 25)" + ), + Iteration("2+5==5+2", "answer=2+5", "input=5+2"), + Iteration("5+2==5+2", "answer=5+2", "input=5+2"), + Iteration("6 − (− 4)==6 − (− 4)", "answer=6 − (− 4)", "input=6 − (− 4)"), + Iteration("6-(-4)==6 − (− 4)", "answer=6-(-4)", "input=6 − (− 4)"), + Iteration("− (− 4) + 6==6 − (− 4)", "answer=− (− 4) + 6", "input=6 − (− 4)"), + Iteration("10==6 − (− 4)", "answer=10", "input=6 − (− 4)"), + Iteration("6 + 4==6 − (− 4)", "answer=6 + 4", "input=6 − (− 4)"), + Iteration("6 + 2^2==6 − (− 4)", "answer=6 + 2^2", "input=6 − (− 4)"), + Iteration("3 * 2 − (− 4)==6 − (− 4)", "answer=3 * 2 − (− 4)", "input=6 − (− 4)"), + Iteration("100/10==6 − (− 4)", "answer=100/10", "input=6 − (− 4)"), + Iteration("3 * 10^-5==3 * 10^-5", "answer=3 * 10^-5", "input=3 * 10^-5"), + Iteration("3/(10 * 10^4)==3 * 10^-5", "answer=3/(10 * 10^4)", "input=3 * 10^-5"), + Iteration( + "1000 + 200 + 30 + 4 + 0.5 + 0.06==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "200 + 30 + 4 + 0.5 + 0.06 + 1000==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=200 + 30 + 4 + 0.5 + 0.06 + 1000", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "0.06 + 0.5 + 4 + 30 + 200 + 1000==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=0.06 + 0.5 + 4 + 30 + 200 + 1000", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1234.56==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1234.56", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "123456/100==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=123456/100", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "61728/50==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=61728/50", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1234 + 56/100==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1234 + 56/100", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1230 + 4.56==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1230 + 4.56", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration("2 * 2 * 3 * 3==2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration( + "2 * 2 * 3 * 3 * 1==2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3 * 1", "input=2 * 2 * 3 * 3" + ), + Iteration("2 * 2 * 9==2 * 2 * 3 * 3", "answer=2 * 2 * 9", "input=2 * 2 * 3 * 3"), + Iteration("4 * 3^2==2 * 2 * 3 * 3", "answer=4 * 3^2", "input=2 * 2 * 3 * 3"), + Iteration("8/2 * 3 * 3==2 * 2 * 3 * 3", "answer=8/2 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration("36==2 * 2 * 3 * 3", "answer=36", "input=2 * 2 * 3 * 3"), + Iteration("sqrt(4-2)==sqrt(2)", "answer=sqrt(4-2)", "input=sqrt(2)"), + Iteration("4x^2+20x==4*x^2+20x", "answer=4x^2+20x", "input=4*x^2+20x"), + Iteration("3+x-5==3+x-5", "answer=3+x-5", "input=3+x-5"), + Iteration("Z+A-Z==Z+A-Z", "answer=Z+A-Z", "input=Z+A-Z"), + Iteration("6C - 5A -1==6C - 5A -1", "answer=6C - 5A -1", "input=6C - 5A -1"), + Iteration("5Z-w==5*Z-w", "answer=5Z-w", "input=5*Z-w"), + Iteration("5*Z-w==5*Z-w", "answer=5*Z-w", "input=5*Z-w"), + Iteration("LS-3S+L==L*S-3S+L", "answer=LS-3S+L", "input=L*S-3S+L"), + Iteration("L*S-3S+L==L*S-3S+L", "answer=L*S-3S+L", "input=L*S-3S+L"), + Iteration("L*S-3*S+L==L*S-3S+L", "answer=L*S-3*S+L", "input=L*S-3S+L"), + Iteration("LS-3*S+L==L*S-3S+L", "answer=LS-3*S+L", "input=L*S-3S+L"), + Iteration("9x^2 − 6x + 1==9x^2 − 6x + 1", "answer=9x^2 − 6x + 1", "input=9x^2 − 6x + 1"), + Iteration("c*b-c==c*b-c", "answer=c*b-c", "input=c*b-c"), + Iteration("bc-c==c*b-c", "answer=bc-c", "input=c*b-c"), + Iteration("cb-c==c*b-c", "answer=cb-c", "input=c*b-c"), + Iteration("-c+bc==c*b-c", "answer=-c+bc", "input=c*b-c"), + Iteration("-c+cb==c*b-c", "answer=-c+cb", "input=c*b-c"), + Iteration("x^2+y+4x==x^2+y+4x", "answer=x^2+y+4x", "input=x^2+y+4x"), + Iteration("y+4x+x^2==x^2+y+4x", "answer=y+4x+x^2", "input=x^2+y+4x"), + Iteration("x^2+4x+y==x^2+y+4x", "answer=x^2+4x+y", "input=x^2+y+4x"), + Iteration("Y+5==Y+5", "answer=Y+5", "input=Y+5"), + Iteration("5+Y==Y+5", "answer=5+Y", "input=Y+5"), + Iteration( + "a^2 + b^2 + c^2+ 2a*b + 2a*c + 2bc==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2+ 2a*b + 2a*c + 2bc", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a^2 + b^2 + c^2+ 2a*b + 2bc + 2a*c==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2+ 2a*b + 2bc + 2a*c", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "2a*b + 2bc + 2a*c + a^2 + b^2 + c^2==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=2a*b + 2bc + 2a*c + a^2 + b^2 + c^2", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "2a*b + b^2 + c^2+ a^2 + 2bc + 2a*c==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=2a*b + b^2 + c^2+ a^2 + 2bc + 2a*c", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a*a + b*b + c*c + 2*a*b + 2*a*c + 2*b*c==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a*a + b*b + c*c + 2*a*b + 2*a*c + 2*b*c", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(a+ b)^2 + c^2 + 2bc + 2a*c==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=(a+ b)^2 + c^2 + 2bc + 2a*c", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(a+b+c)^2==a^2+b^2+c^2+2a*b+2a*c+2bc", "answer=(a+b+c)^2", "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(-a -b -c)^2==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=(-a -b -c)^2", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration("1 - 6x + 9x^2==9x^2 − 6x + 1", "answer=1 - 6x + 9x^2", "input=9x^2 − 6x + 1"), + Iteration("9x^2 + 1 - 6x==9x^2 − 6x + 1", "answer=9x^2 + 1 - 6x", "input=9x^2 − 6x + 1"), + Iteration("2+1+x==x+1+2", "answer=2+1+x", "input=x+1+2"), + Iteration("1+2+x==x+1+2", "answer=1+2+x", "input=x+1+2"), + Iteration("1+x+2==x+1+2", "answer=1+x+2", "input=x+1+2"), + Iteration("2+x+1==x+1+2", "answer=2+x+1", "input=x+1+2"), + Iteration("(x+1)+2==x+1+2", "answer=(x+1)+2", "input=x+1+2"), + Iteration("x + (1+2)==x+1+2", "answer=x + (1+2)", "input=x+1+2"), + Iteration( + "y+1+ 9x(x − 6)==9x(x − 6) + 1+ y", "answer=y+1+ 9x(x − 6)", "input=9x(x − 6) + 1+ y" + ), + Iteration("1+y+9x(x − 6)==9x(x − 6) + 1+ y", "answer=1+y+9x(x − 6)", "input=9x(x − 6) + 1+ y"), + Iteration( + "1 + 9x(x − 6) + y==9x(x − 6) + 1+ y", "answer=1 + 9x(x − 6) + y", "input=9x(x − 6) + 1+ y" + ), + Iteration( + "(y+1)+9x(x − 6)==9x(x − 6) + 1+ y", "answer=(y+1)+9x(x − 6)", "input=9x(x − 6) + 1+ y" + ), + Iteration( + "(x^2 − x)/3 − 4y==(x^2 − x)/3 − 4y", "answer=(x^2 − x)/3 − 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "-4y + (x^2 − x)/3==(x^2 − x)/3 − 4y", "answer=-4y + (x^2 − x)/3", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "x(x − 1)/3 −4y==(x^2 − x)/3 − 4y", "answer=x(x − 1)/3 −4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "x^2/3 − x/3 − 4y==(x^2 − x)/3 − 4y", "answer=x^2/3 − x/3 − 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "x^2/3 − (x/3 + 4y)==(x^2 − x)/3 − 4y", "answer=x^2/3 − (x/3 + 4y)", "input=(x^2 − x)/3 − 4y" + ), + Iteration("(3x -1)^2==(3x-1)^2", "answer=(3x -1)^2", "input=(3x-1)^2"), + Iteration("2*(6+3+4) + 4==2*(2+6+3+4)", "answer=2*(6+3+4) + 4", "input=2*(2+6+3+4)"), + Iteration("2*(2+6+3) + 8==2*(2+6+3+4)", "answer=2*(2+6+3) + 8", "input=2*(2+6+3+4)"), + Iteration("(2+6+3+4)*2==2*(2+6+3+4)", "answer=(2+6+3+4)*2", "input=2*(2+6+3+4)"), + Iteration("(2+6+3+4) × 2==2*(2+6+3+4)", "answer=(2+6+3+4) × 2", "input=2*(2+6+3+4)"), + Iteration("15 - 12 + 3==15 - (6 × 2) + 3", "answer=15 - 12 + 3", "input=15 - (6 × 2) + 3"), + Iteration( + "3 - (6 * 2) + 15==15 - (6 × 2) + 3", "answer=3 - (6 * 2) + 15", "input=15 - (6 × 2) + 3" + ), + Iteration( + "15 - (2 × 6) + 3==15 - (6 × 2) + 3", "answer=15 - (2 × 6) + 3", "input=15 - (6 × 2) + 3" + ), + Iteration( + "2 *(50 + 150) + 2*(100 + 25)==(50 + 150 + 100 + 25) × 2", + "answer=2 *(50 + 150) + 2*(100 + 25)", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration( + "2* ( 25+50+100+150)==(50 + 150 + 100 + 25) × 2", + "answer=2* ( 25+50+100+150)", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration("10^−5 * 3==3 * 10^-5", "answer=10^−5 * 3", "input=3 * 10^-5"), + Iteration("30 * 10^−6==3 * 10^-5", "answer=30 * 10^−6", "input=3 * 10^-5"), + Iteration("0.00003==3 * 10^-5", "answer=0.00003", "input=3 * 10^-5"), + Iteration("3/10^5==3 * 10^-5", "answer=3/10^5", "input=3 * 10^-5"), + Iteration("3 *2 – (− 4)==6 − (− 4)", "answer=3 *2 – (− 4)", "input=6 − (− 4)"), + Iteration("7==5+2", "answer=7", "input=5+2"), + Iteration("3+4==5+2", "answer=3+4", "input=5+2"), + Iteration("20x+4x^2==4*x^2+20x", "answer=20x+4x^2", "input=4*x^2+20x"), + Iteration("x-5+3==3+x-5", "answer=x-5+3", "input=3+x-5"), + Iteration("-5+3+x==3+x-5", "answer=-5+3+x", "input=3+x-5"), + Iteration("-5+x+3==3+x-5", "answer=-5+x+3", "input=3+x-5"), + Iteration("3+(x-5)==3+x-5", "answer=3+(x-5)", "input=3+x-5"), + Iteration("A==Z+A-Z", "answer=A", "input=Z+A-Z"), + Iteration("A+Z-Z==Z+A-Z", "answer=A+Z-Z", "input=Z+A-Z"), + Iteration("Z+(A-Z)==Z+A-Z", "answer=Z+(A-Z)", "input=Z+A-Z"), + Iteration("6C - (5A+1)==6C - 5A -1", "answer=6C - (5A+1)", "input=6C - 5A -1"), + Iteration("-5A-1+6C==6C - 5A -1", "answer=-5A-1+6C", "input=6C - 5A -1"), + Iteration("-W+5Z==5*Z-W", "answer=-W+5Z", "input=5*Z-W"), + Iteration("L(1+S)-3S==L*S-3S+L", "answer=L(1+S)-3S", "input=L*S-3S+L"), + Iteration("S(L-3)+L==L*S-3S+L", "answer=S(L-3)+L", "input=L*S-3S+L"), + Iteration("L+LS-3S==L*S-3S+L", "answer=L+LS-3S", "input=L*S-3S+L"), + Iteration( + "x(x − 1)/3 − 4y==(x^2 − x)/3 − 4y", "answer=x(x − 1)/3 − 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "- 4y + (x^2 − x)/3==(x^2 − x)/3 − 4y", "answer=- 4y + (x^2 − x)/3", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(x^2 − x) * 3^-1 − 4y==(x^2 − x)/3 − 4y", + "answer=(x^2 − x) * 3^-1 − 4y", + "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "a^2 + b^2 + c^2 + 2(a*b + a*c + bc)==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2 + 2(a*b + a*c + bc)", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(a + b)^2 + c^2 + 2a*c + 2bc==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=(a + b)^2 + c^2 + 2a*c + 2bc", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a * a + b * b + c^3/c + 2a*b + 2a*c + 2bc==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a * a + b * b + c^3/c + 2a*b + 2a*c + 2bc", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a^2+ b^2 + c^2 + 2bc + 2a*c + 2a*b==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2+ b^2 + c^2 + 2bc + 2a*c + 2a*b", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration("(3x − 1)^2==9x^2 − 6x + 1", "answer=(3x − 1)^2", "input=9x^2 − 6x + 1"), + Iteration("3x(3x − 2) + 1==9x^2 − 6x + 1", "answer=3x(3x − 2) + 1", "input=9x^2 − 6x + 1"), + Iteration("3(3x^2 − 2x) + 1==9x^2 − 6x + 1", "answer=3(3x^2 − 2x) + 1", "input=9x^2 − 6x + 1"), + Iteration("(3x)^2 − 6x + 1==9x^2 − 6x + 1", "answer=(3x)^2 − 6x + 1", "input=9x^2 − 6x + 1"), + Iteration("c(b-1)==c*b-c", "answer=c(b-1)", "input=c*b-c"), + Iteration("x(x+4)+y==x^2+y+4x", "answer=x(x+4)+y", "input=x^2+y+4x"), + Iteration("x+3==x+1+2", "answer=x+3", "input=x+1+2") + ) + fun testMatches_assortedExpressions_withMatchingCharacteristics_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // pass for this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("√(3x −1)4!=(3x-1)^2", "answer=√(3x −1)4", "input=(3x-1)^2"), + Iteration("3 * 10^5!=3 * 10^-5", "answer=3 * 10^5", "input=3 * 10^-5"), + Iteration("2 * 10^−5!=3 * 10^-5", "answer=2 * 10^−5", "input=3 * 10^-5"), + Iteration("5 * 10^−3!=3 * 10^-5", "answer=5 * 10^−3", "input=3 * 10^-5"), + Iteration( + "123456!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=123456", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1000 + 200 + 30!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1000 + 200 + 30", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration("6 − 4!=6 − (− 4)", "answer=6 − 4", "input=6 − (− 4)"), + Iteration("6 + (− 4)!=6 − (− 4)", "answer=6 + (− 4)", "input=6 − (− 4)"), + Iteration("100!=6 − (− 4)", "answer=100", "input=6 − (− 4)"), + Iteration("2 * 2 * 3!=2 * 2 * 3 * 3", "answer=2 * 2 * 3", "input=2 * 2 * 3 * 3"), + Iteration("2 * 3 * 3 * 3!=2 * 2 * 3 * 3", "answer=2 * 3 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration( + "x(x^2 − x)/3 − 4y!=(x^2 − x)/3 − 4y", "answer=x(x^2 − x)/3 − 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(x^2 − x)/3 + 4y!=(x^2 − x)/3 − 4y", "answer=(x^2 − x)/3 + 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(x^2 + x)/3 - 4y!=(x^2 − x)/3 − 4y", "answer=(x^2 + x)/3 - 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(x^2 − x)*0.33 - 4y!=(x^2 − x)/3 − 4y", + "answer=(x^2 − x)*0.33 - 4y", + "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(a + b + c)^3!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=(a + b + c)^3", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a^2 + b^2 + c^2!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a^2 + b^2 + c^2- 2a*b - 2a*c - 2bc!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2- 2a*b - 2a*c - 2bc", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration("9x^2 - 6x - 1!=(3x-1)^2", "answer=9x^2 - 6x - 1", "input=(3x-1)^2"), + Iteration("(3x −1)!=(3x-1)^2", "answer=(3x −1)", "input=(3x-1)^2"), + Iteration("2x!=sqrt(2x)^2", "answer=2x", "input=sqrt(2x)^2"), + Iteration("2x!=sqrt(-4x^2)", "answer=2x", "input=sqrt(-4x^2)"), + Iteration("x^2+2x+1!=(x+2)^2", "answer=x^2+2x+1", "input=(x+2)^2"), + Iteration("x^2-1!=(x+1)(1-x)", "answer=x^2-1", "input=(x+1)(1-x)"), + Iteration("x+1!=(x^2+2x+1)/(x-1)", "answer=x+1", "input=(x^2+2x+1)/(x-1)"), + Iteration("x-1!=(x^2-1)/x", "answer=x-1", "input=(x^2-1)/x"), + Iteration("x+1!=(x^2-1)/(x-2)", "answer=x+1", "input=(x^2-1)/(x-2)"), + Iteration("-3x!=(9x^3)^(1/3)", "answer=-3x", "input=(9x^3)^(1/3)"), + Iteration("Y==Y+5", "answer=Y", "input=Y+5"), + Iteration("5==Y+5", "answer=5", "input=Y+5") + ) + fun testMatches_assortedExpressions_withoutMatchingCharacteristics_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // not pass for this classifier. + assertThat(matches).isFalse() + } + + private fun matchesClassifier( + answerExpression: InteractionObject, + inputExpression: InteractionObject, + allowedVariables: List = allPossibleVariables + ): Boolean { + return classifier.matches( + answerExpression, + inputs = mapOf("x" to inputExpression), + classificationContext = ClassificationContext( + customizationArgs = mapOf( + "customOskLetters" to SchemaObject.newBuilder().apply { + schemaObjectList = SchemaObjectList.newBuilder().apply { + addAllSchemaObject( + allowedVariables.map { + SchemaObject.newBuilder().setNormalizedString(it).build() + } + ) + }.build() + }.build() + ) + ) + ) + } + + private fun createMathExpression(rawExpression: String) = InteractionObject.newBuilder().apply { + mathExpression = rawExpression + }.build() + + private fun setUpTestApplicationComponent() { + DaggerAlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestDispatcherModule::class, LoggerModule::class, RobolectricModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(test: AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest) + } +} diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt new file mode 100644 index 00000000000..0c6e401a976 --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt @@ -0,0 +1,649 @@ +package org.oppia.android.domain.classify.rules.algebraicexpressioninput + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner +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.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton +import org.oppia.android.app.model.SchemaObject +import org.oppia.android.app.model.SchemaObjectList +import org.oppia.android.domain.classify.ClassificationContext + +/** + * Tests for [AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider]. + * + * Note that the tests implemented in this suite are specifically set up to verify the cases + * outlined in this sheet: + * https://docs.google.com/spreadsheets/d/1u1fQdah2WsmdYKWKGmuXy5TPT7Ot-b8A7O9iZF-j5XE/edit#gid=0. + */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest { + @Inject + internal lateinit var provider: AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider + + @Parameter lateinit var answer: String + @Parameter lateinit var input: String + + private lateinit var classifier: RuleClassifier + private val allPossibleVariables = (('a'..'z') + ('A'..'Z')).toList().map { it.toString() } + + @Before + fun setUp() { + setUpTestApplicationComponent() + classifier = provider.createRuleClassifier() + } + + @Test + fun testMatches_answerHasDisallowedVariable_returnsFalse() { + val answerExpression = createMathExpression("y") + val inputExpression = createMathExpression("y") + + val matches = matchesClassifier(answerExpression, inputExpression, allowedVariables = listOf()) + + // Despite the answer otherwise being equal, the variable isn't allowed. This shouldn't actually + // be the case in practice since neither the creator nor the learner would be allowed to input a + // disallowed variable (so this check is mainly a "just-in-case"). + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("0==0", "answer=0", "input=0"), + Iteration("1==1", "answer=1", "input=1"), + Iteration("2==2", "answer=2", "input=2"), + Iteration("x==x", "answer=x", "input=x"), + Iteration("y==y", "answer=y", "input=y") + ) + fun testMatches_sameSingleTerms_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("-2==-2", "answer=-2", "input=-2"), + Iteration("1+3.14==1+3.14", "answer=1+3.14", "input=1+3.14"), + Iteration(" 1 + 3.14 ==1+3.14", "answer= 1 + 3.14 ", "input=1+3.14"), + Iteration("1+2+3==1+2+3", "answer=1+2+3", "input=1+2+3"), + Iteration("1-3.14==1-3.14", "answer=1-3.14", "input=1-3.14"), + Iteration("2*3.14==2*3.14", "answer=2*3.14", "input=2*3.14"), + Iteration("2/3==2/3", "answer=2/3", "input=2/3"), + Iteration("2/3.14==2/3.14", "answer=2/3.14", "input=2/3.14"), + Iteration("2^3==2^3", "answer=2^3", "input=2^3"), + Iteration("2^3.14==2^3.14", "answer=2^3.14", "input=2^3.14"), + Iteration("sqrt(2)==sqrt(2)", "answer=sqrt(2)", "input=sqrt(2)"), + Iteration("-x==-x", "answer=-x", "input=-x"), + Iteration("x+3.14==x+3.14", "answer=x+3.14", "input=x+3.14"), + Iteration("x-3.14==x-3.14", "answer=x-3.14", "input=x-3.14"), + Iteration("x*3.14==x*3.14", "answer=x*3.14", "input=x*3.14"), + Iteration("x/3==x/3", "answer=x/3", "input=x/3"), + Iteration("sqrt(x)==sqrt(x)", "answer=sqrt(x)", "input=sqrt(x)") + ) + fun testMatches_sameSingleOperations_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("1!=0", "answer=1", "input=0"), + Iteration("0!=1", "answer=0", "input=1"), + Iteration("3.14!=1", "answer=3.14", "input=1"), + Iteration("1!=3.14", "answer=1", "input=3.14"), + Iteration("x!=3.14", "answer=x", "input=3.14"), + Iteration("y!=x", "answer=y", "input=x"), + Iteration("3.14!=x", "answer=3.14", "input=x") + ) + fun testMatches_differentSingleTerms_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions aren't exactly the same (minus whitespace), they won't match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("3.14+1!=1+3.14", "answer=3.14+1", "input=1+3.14"), + Iteration("3+2+1!=1+2+3", "answer=3+2+1", "input=1+2+3"), + Iteration("-3.14+1!=1-3.14", "answer=-3.14+1", "input=1-3.14"), + Iteration("3.14*2!=2*3.14", "answer=3.14*2", "input=2*3.14"), + Iteration("2+x!=x+2", "answer=2+x", "input=x+2"), + Iteration("y+x!=x+y", "answer=y+x", "input=x+y"), + Iteration("x*2!=2x", "answer=x*2", "input=2x"), + Iteration("yx!=xy", "answer=yx", "input=xy") + ) + fun testMatches_operationsDiffer_byCommutativity_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier expects commutativity to be retained. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("1+(2+3)!=(1+2)+3", "answer=1+(2+3)", "input=(1+2)+3"), + Iteration("2*(3*4)!=(2*3)*4", "answer=2*(3*4)", "input=(2*3)*4"), + Iteration("x+(2+3)!=(x+2)+3", "answer=x+(2+3)", "input=(x+2)+3"), + Iteration("x+(y+z)!=(x+y)+z", "answer=x+(y+z)", "input=(x+y)+z"), + Iteration("2*(3x)!=(2x)*3", "answer=2*(3x)", "input=(2x)*3"), + Iteration("x(yz)!=(xy)z", "answer=x(yz)", "input=(xy)z") + ) + fun testMatches_operationsDiffer_byAssociativity_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier expects associativity to be retained. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("3.14-1!=1-3.14", "answer=3.14-1", "input=1-3.14"), + Iteration("1-(2-3)!=(1-2)-3", "answer=1-(2-3)", "input=(1-2)-3"), + Iteration("3.14/2!=2/3.14", "answer=3.14/2", "input=2/3.14"), + Iteration("2/(3/4)!=(2/3)/4", "answer=2/(3/4)", "input=(2/3)/4"), + Iteration("3.14^2!=2^3.14", "answer=3.14^2", "input=2^3.14"), + Iteration("3.14-x!=x-3.14", "answer=3.14-x", "input=x-3.14"), + Iteration("x-(y-z)!=(x-y)-z", "answer=x-(y-z)", "input=(x-y)-z"), + Iteration("3.14/x!=x/3.14", "answer=3.14/x", "input=x/3.14"), + Iteration("x/(y/z)!=(x/y)/z", "answer=x/(y/z)", "input=(x/y)/z") + ) + fun testMatches_operationsDiffer_byNonCommutativeOrAssociativeReordering_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Non-commutative and non-associative reordering generally results in a different value, so the + // classifier will fail to match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("1-2!=-(2-1)", "answer=1-2", "input=-(2-1)"), + Iteration("1+2!=1+1+1", "answer=1+2", "input=1+1+1"), + Iteration("1+2!=1-(-2)", "answer=1+2", "input=1-(-2)"), + Iteration("4-6!=1-2-1", "answer=4-6", "input=1-2-1"), + Iteration("2*3*2*2!=2*3*4", "answer=2*3*2*2", "input=2*3*4"), + Iteration("-6-2!=2*-(3+1)", "answer=-6-2", "input=2*-(3+1)"), + Iteration("2/3/2/2!=2/3/4", "answer=2/3/2/2", "input=2/3/4"), + Iteration("2^(2+1)!=2^3", "answer=2^(2+1)", "input=2^3"), + Iteration("2^(-1)!=1/2", "answer=2^(-1)", "input=1/2"), + Iteration("x-y!=-(y-x)", "answer=x-y", "input=-(y-x)"), + Iteration("2+x!=1+x+1", "answer=2+x", "input=1+x+1"), + Iteration("1+x!=1-(-x)", "answer=1+x", "input=1-(-x)"), + Iteration("x!=1-x-1", "answer=x", "input=1-x-1"), + Iteration("4x!=2*2*x", "answer=4x", "input=2*2*x"), + Iteration("2-6x!=2*(-3x+1)", "answer=2-6x", "input=2*(-3x+1)"), + Iteration("x/4!=x/2/2", "answer=x/4", "input=x/2/2"), + Iteration("x^(2+1)!=x^3", "answer=x^(2+1)", "input=x^3"), + Iteration("x*(2^(-1))!=x/2", "answer=x*(2^(-1))", "input=x/2") + ) + fun testMatches_operationsDiffer_byDistributionAndCombining_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier doesn't support distributing or combining terms. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("2*(2+6+3+4)==2*(2+6+3+4)", "answer=2*(2+6+3+4)", "input=2*(2+6+3+4)"), + Iteration("2 × (2+6+3+4)==2*(2+6+3+4)", "answer=2 × (2+6+3+4)", "input=2*(2+6+3+4)"), + Iteration( + "15 - (6 × 2) + 3==15 - (6 × 2) + 3", "answer=15 - (6 × 2) + 3", "input=15 - (6 × 2) + 3" + ), + Iteration( + "2 * (50 + 150 + 100 + 25) ==2 × (50 + 150 + 100 + 25)", + "answer=2 * (50 + 150 + 100 + 25) ", + "input=2 × (50 + 150 + 100 + 25)" + ), + Iteration("5+2==5+2", "answer=5+2", "input=5+2"), + Iteration("6 − (− 4)==6 − (− 4)", "answer=6 − (− 4)", "input=6 − (− 4)"), + Iteration("6-(-4)==6 − (− 4)", "answer=6-(-4)", "input=6 − (− 4)"), + Iteration("3 * 10^-5==3 * 10^-5", "answer=3 * 10^-5", "input=3 * 10^-5"), + Iteration( + "1000 + 200 + 30 + 4 + 0.5 + 0.06==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration("2 * 2 * 3 * 3==2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration("4x^2+20x==4*x^2+20x", "answer=4x^2+20x", "input=4*x^2+20x"), + Iteration("3+x-5==3+x-5", "answer=3+x-5", "input=3+x-5"), + Iteration("Z+A-Z==Z+A-Z", "answer=Z+A-Z", "input=Z+A-Z"), + Iteration("6C - 5A -1==6C - 5A -1", "answer=6C - 5A -1", "input=6C - 5A -1"), + Iteration("5Z-w==5*Z-w", "answer=5Z-w", "input=5*Z-w"), + Iteration("5*Z-w==5*Z-w", "answer=5*Z-w", "input=5*Z-w"), + Iteration("LS-3S+L==L*S-3S+L", "answer=LS-3S+L", "input=L*S-3S+L"), + Iteration("L*S-3S+L==L*S-3S+L", "answer=L*S-3S+L", "input=L*S-3S+L"), + Iteration("L*S-3*S+L==L*S-3S+L", "answer=L*S-3*S+L", "input=L*S-3S+L"), + Iteration("LS-3*S+L==L*S-3S+L", "answer=LS-3*S+L", "input=L*S-3S+L"), + Iteration("9x^2 − 6x + 1==9x^2 − 6x + 1", "answer=9x^2 − 6x + 1", "input=9x^2 − 6x + 1"), + Iteration("c*b-c==c*b-c", "answer=c*b-c", "input=c*b-c"), + Iteration("cb-c==c*b-c", "answer=cb-c", "input=c*b-c"), + Iteration("x^2+y+4x==x^2+y+4x", "answer=x^2+y+4x", "input=x^2+y+4x"), + Iteration("Y+5==Y+5", "answer=Y+5", "input=Y+5"), + Iteration( + "a^2 + b^2 + c^2+ 2a*b + 2a*c + 2bc==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2+ 2a*b + 2a*c + 2bc", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(x^2 − x)/3 − 4y==(x^2 − x)/3 − 4y", "answer=(x^2 − x)/3 − 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration("(3x -1)^2==(3x-1)^2", "answer=(3x -1)^2", "input=(3x-1)^2") + ) + fun testMatches_assortedExpressions_withMatchingCharacteristics_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // pass for this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration( + "2 × (50 + 150 + 100 + 25) ==(50 + 150 + 100 + 25) × 2", + "answer=2 × (50 + 150 + 100 + 25) ", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration("2+5!=5+2", "answer=2+5", "input=5+2"), + Iteration("− (− 4) + 6!=6 − (− 4)", "answer=− (− 4) + 6", "input=6 − (− 4)"), + Iteration("10!=6 − (− 4)", "answer=10", "input=6 − (− 4)"), + Iteration("6 + 4!=6 − (− 4)", "answer=6 + 4", "input=6 − (− 4)"), + Iteration("6 + 2^2!=6 − (− 4)", "answer=6 + 2^2", "input=6 − (− 4)"), + Iteration("3 * 2 − (− 4)!=6 − (− 4)", "answer=3 * 2 − (− 4)", "input=6 − (− 4)"), + Iteration("100/10!=6 − (− 4)", "answer=100/10", "input=6 − (− 4)"), + Iteration("3/(10 * 10^4)!=3 * 10^-5", "answer=3/(10 * 10^4)", "input=3 * 10^-5"), + Iteration( + "200 + 30 + 4 + 0.5 + 0.06 + 1000!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=200 + 30 + 4 + 0.5 + 0.06 + 1000", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "0.06 + 0.5 + 4 + 30 + 200 + 1000!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=0.06 + 0.5 + 4 + 30 + 200 + 1000", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1234.56!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1234.56", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "123456/100!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=123456/100", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "61728/50!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=61728/50", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1234 + 56/100!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1234 + 56/100", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1230 + 4.56!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1230 + 4.56", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "2 * 2 * 3 * 3 * 1!=2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3 * 1", "input=2 * 2 * 3 * 3" + ), + Iteration("2 * 2 * 9!=2 * 2 * 3 * 3", "answer=2 * 2 * 9", "input=2 * 2 * 3 * 3"), + Iteration("4 * 3^2!=2 * 2 * 3 * 3", "answer=4 * 3^2", "input=2 * 2 * 3 * 3"), + Iteration("8/2 * 3 * 3!=2 * 2 * 3 * 3", "answer=8/2 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration("36!=2 * 2 * 3 * 3", "answer=36", "input=2 * 2 * 3 * 3"), + Iteration("sqrt(4-2)!=sqrt(2)", "answer=sqrt(4-2)", "input=sqrt(2)"), + Iteration("bc-c!=c*b-c", "answer=bc-c", "input=c*b-c"), + Iteration("-c+bc!=c*b-c", "answer=-c+bc", "input=c*b-c"), + Iteration("-c+cb!=c*b-c", "answer=-c+cb", "input=c*b-c"), + Iteration("y+4x+x^2!=x^2+y+4x", "answer=y+4x+x^2", "input=x^2+y+4x"), + Iteration("x^2+4x+y!=x^2+y+4x", "answer=x^2+4x+y", "input=x^2+y+4x"), + Iteration("5+Y!=Y+5", "answer=5+Y", "input=Y+5"), + Iteration( + "a^2 + b^2 + c^2+ 2a*b + 2bc + 2a*c!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2+ 2a*b + 2bc + 2a*c", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "2a*b + 2bc + 2a*c + a^2 + b^2 + c^2!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=2a*b + 2bc + 2a*c + a^2 + b^2 + c^2", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "2a*b + b^2 + c^2+ a^2 + 2bc + 2a*c!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=2a*b + b^2 + c^2+ a^2 + 2bc + 2a*c", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a*a + b*b + c*c + 2*a*b + 2*a*c + 2*b*c!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a*a + b*b + c*c + 2*a*b + 2*a*c + 2*b*c", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(a+ b)^2 + c^2 + 2bc + 2a*c!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=(a+ b)^2 + c^2 + 2bc + 2a*c", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(a+b+c)^2!=a^2+b^2+c^2+2a*b+2a*c+2bc", "answer=(a+b+c)^2", "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(-a -b -c)^2!=a^2+b^2+c^2+2a*b+2a*c+2bc", "answer=(-a -b -c)^2", "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration("1 - 6x + 9x^2!=9x^2 − 6x + 1", "answer=1 - 6x + 9x^2", "input=9x^2 − 6x + 1"), + Iteration("9x^2 + 1 - 6x!=9x^2 − 6x + 1", "answer=9x^2 + 1 - 6x", "input=9x^2 − 6x + 1"), + Iteration("2+1+x!=x+1+2", "answer=2+1+x", "input=x+1+2"), + Iteration("1+2+x!=x+1+2", "answer=1+2+x", "input=x+1+2"), + Iteration("1+x+2!=x+1+2", "answer=1+x+2", "input=x+1+2"), + Iteration("2+x+1!=x+1+2", "answer=2+x+1", "input=x+1+2"), + Iteration("(x+1)+2!=x+1+2", "answer=(x+1)+2", "input=x+1+2"), + Iteration("x + (1+2)!=x+1+2", "answer=x + (1+2)", "input=x+1+2"), + Iteration( + "y+1+ 9x(x − 6)!=9x(x − 6) + 1+ y", "answer=y+1+ 9x(x − 6)", "input=9x(x − 6) + 1+ y" + ), + Iteration("1+y+9x(x − 6)!=9x(x − 6) + 1+ y", "answer=1+y+9x(x − 6)", "input=9x(x − 6) + 1+ y"), + Iteration( + "1 + 9x(x − 6) + y!=9x(x − 6) + 1+ y", "answer=1 + 9x(x − 6) + y", "input=9x(x − 6) + 1+ y" + ), + Iteration( + "(y+1)+9x(x − 6)!=9x(x − 6) + 1+ y", "answer=(y+1)+9x(x − 6)", "input=9x(x − 6) + 1+ y" + ), + Iteration( + "-4y + (x^2 − x)/3!=(x^2 − x)/3 − 4y", "answer=-4y + (x^2 − x)/3", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "x(x − 1)/3 −4y!=(x^2 − x)/3 − 4y", "answer=x(x − 1)/3 −4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "x^2/3 − x/3 − 4y!=(x^2 − x)/3 − 4y", "answer=x^2/3 − x/3 − 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "x^2/3 − (x/3 + 4y)!=(x^2 − x)/3 − 4y", "answer=x^2/3 − (x/3 + 4y)", "input=(x^2 − x)/3 − 4y" + ), + Iteration("√(3x −1)4!=(3x-1)^2", "answer=√(3x −1)4", "input=(3x-1)^2"), + Iteration("3x(3x - 2) + 1!=(3x-1)^2", "answer=3x(3x - 2) + 1", "input=(3x-1)^2"), + Iteration("3(3x^2) - 6x +1!=(3x-1)^2", "answer=3(3x^2) - 6x +1", "input=(3x-1)^2"), + Iteration("2x!=sqrt(4x^2)", "answer=2x", "input=sqrt(4x^2)"), + Iteration("x^2+2x+1!=(x+1)^2", "answer=x^2+2x+1", "input=(x+1)^2"), + Iteration("x^2-1!=(x+1)(x-1)", "answer=x^2-1", "input=(x+1)(x-1)"), + Iteration("x+1!=(x^2+2x+1)/(x+1)", "answer=x+1", "input=(x^2+2x+1)/(x+1)"), + Iteration("x-1!=(x^2-1)/(x+1)", "answer=x-1", "input=(x^2-1)/(x+1)"), + Iteration("x+1!=(x^2-1)/(x-1)", "answer=x+1", "input=(x^2-1)/(x-1)"), + Iteration("-3x!=(-27x^3)^(1/3)", "answer=-3x", "input=(-27x^3)^(1/3)"), + Iteration("1!=(x^2-1)/(x^2-1)", "answer=1", "input=(x^2-1)/(x^2-1)"), + Iteration("2*(6+3+4) + 4!=2*(2+6+3+4)", "answer=2*(6+3+4) + 4", "input=2*(2+6+3+4)"), + Iteration("2*(2+6+3) + 8!=2*(2+6+3+4)", "answer=2*(2+6+3) + 8", "input=2*(2+6+3+4)"), + Iteration("(2+6+3+4)*2!=2*(2+6+3+4)", "answer=(2+6+3+4)*2", "input=2*(2+6+3+4)"), + Iteration("(2+6+3+4) × 2!=2*(2+6+3+4)", "answer=(2+6+3+4) × 2", "input=2*(2+6+3+4)"), + Iteration("15 - 12 + 3!=15 - (6 × 2) + 3", "answer=15 - 12 + 3", "input=15 - (6 × 2) + 3"), + Iteration( + "3 - (6 * 2) + 15!=15 - (6 × 2) + 3", "answer=3 - (6 * 2) + 15", "input=15 - (6 × 2) + 3" + ), + Iteration( + "15 - (2 × 6) + 3!=15 - (6 × 2) + 3", "answer=15 - (2 × 6) + 3", "input=15 - (6 × 2) + 3" + ), + Iteration( + "2 *(50 + 150) + 2*(100 + 25)!=(50 + 150 + 100 + 25) × 2", + "answer=2 *(50 + 150) + 2*(100 + 25)", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration( + "2* ( 25+50+100+150)!=(50 + 150 + 100 + 25) × 2", + "answer=2* ( 25+50+100+150)", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration("10^−5 * 3!=3 * 10^-5", "answer=10^−5 * 3", "input=3 * 10^-5"), + Iteration("3 * 10^5!=3 * 10^-5", "answer=3 * 10^5", "input=3 * 10^-5"), + Iteration("2 * 10^−5!=3 * 10^-5", "answer=2 * 10^−5", "input=3 * 10^-5"), + Iteration("5 * 10^−3!=3 * 10^-5", "answer=5 * 10^−3", "input=3 * 10^-5"), + Iteration("30 * 10^−6!=3 * 10^-5", "answer=30 * 10^−6", "input=3 * 10^-5"), + Iteration("0.00003!=3 * 10^-5", "answer=0.00003", "input=3 * 10^-5"), + Iteration("3/10^5!=3 * 10^-5", "answer=3/10^5", "input=3 * 10^-5"), + Iteration( + "123456!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=123456", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1000 + 200 + 30!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1000 + 200 + 30", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration("3 *2 – (− 4)!=6 − (− 4)", "answer=3 *2 – (− 4)", "input=6 − (− 4)"), + Iteration("6 − 4!=6 − (− 4)", "answer=6 − 4", "input=6 − (− 4)"), + Iteration("6 + (− 4)!=6 − (− 4)", "answer=6 + (− 4)", "input=6 − (− 4)"), + Iteration("100!=6 − (− 4)", "answer=100", "input=6 − (− 4)"), + Iteration("7!=5+2", "answer=7", "input=5+2"), + Iteration("3+4!=5+2", "answer=3+4", "input=5+2"), + Iteration("2 * 2 * 3!=2 * 2 * 3 * 3", "answer=2 * 2 * 3", "input=2 * 2 * 3 * 3"), + Iteration("2 * 3 * 3 * 3!=2 * 2 * 3 * 3", "answer=2 * 3 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration("20x+4x^2!=4*x^2+20x", "answer=20x+4x^2", "input=4*x^2+20x"), + Iteration("x-5+3!=3+x-5", "answer=x-5+3", "input=3+x-5"), + Iteration("-5+3+x!=3+x-5", "answer=-5+3+x", "input=3+x-5"), + Iteration("-5+x+3!=3+x-5", "answer=-5+x+3", "input=3+x-5"), + Iteration("3+(x-5)!=3+x-5", "answer=3+(x-5)", "input=3+x-5"), + Iteration("A!=Z+A-Z", "answer=A", "input=Z+A-Z"), + Iteration("A+Z-Z!=Z+A-Z", "answer=A+Z-Z", "input=Z+A-Z"), + Iteration("Z+(A-Z)!=Z+A-Z", "answer=Z+(A-Z)", "input=Z+A-Z"), + Iteration("6C - (5A+1)!=6C - 5A -1", "answer=6C - (5A+1)", "input=6C - 5A -1"), + Iteration("-5A-1+6C!=6C - 5A -1", "answer=-5A-1+6C", "input=6C - 5A -1"), + Iteration("-W+5Z!=5*Z-W", "answer=-W+5Z", "input=5*Z-W"), + Iteration("L(1+S)-3S!=L*S-3S+L", "answer=L(1+S)-3S", "input=L*S-3S+L"), + Iteration("S(L-3)+L!=L*S-3S+L", "answer=S(L-3)+L", "input=L*S-3S+L"), + Iteration("L+LS-3S!=L*S-3S+L", "answer=L+LS-3S", "input=L*S-3S+L"), + Iteration( + "x(x − 1)/3 − 4y!=(x^2 − x)/3 − 4y", "answer=x(x − 1)/3 − 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "- 4y + (x^2 − x)/3!=(x^2 − x)/3 − 4y", "answer=- 4y + (x^2 − x)/3", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(x^2 − x) * 3^-1 − 4y!=(x^2 − x)/3 − 4y", + "answer=(x^2 − x) * 3^-1 − 4y", + "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "x(x^2 − x)/3 − 4y!=(x^2 − x)/3 − 4y", "answer=x(x^2 − x)/3 − 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(x^2 − x)/3 + 4y!=(x^2 − x)/3 − 4y", "answer=(x^2 − x)/3 + 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(x^2 + x)/3 - 4y!=(x^2 − x)/3 − 4y", "answer=(x^2 + x)/3 - 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(x^2 − x)*0.33 - 4y!=(x^2 − x)/3 − 4y", + "answer=(x^2 − x)*0.33 - 4y", + "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "a^2 + b^2 + c^2 + 2(a*b + a*c + bc)!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2 + 2(a*b + a*c + bc)", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(a + b)^2 + c^2 + 2a*c + 2bc!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=(a + b)^2 + c^2 + 2a*c + 2bc", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a * a + b * b + c^3/c + 2a*b + 2a*c + 2bc!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a * a + b * b + c^3/c + 2a*b + 2a*c + 2bc", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a^2+ b^2 + c^2 + 2bc + 2a*c + 2a*b!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2+ b^2 + c^2 + 2bc + 2a*c + 2a*b", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(a + b + c)^3!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=(a + b + c)^3", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a^2 + b^2 + c^2!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a^2 + b^2 + c^2- 2a*b - 2a*c - 2bc!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2- 2a*b - 2a*c - 2bc", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration("(3x − 1)^2!=9x^2 − 6x + 1", "answer=(3x − 1)^2", "input=9x^2 − 6x + 1"), + Iteration("3x(3x − 2) + 1!=9x^2 − 6x + 1", "answer=3x(3x − 2) + 1", "input=9x^2 − 6x + 1"), + Iteration("3(3x^2 − 2x) + 1!=9x^2 − 6x + 1", "answer=3(3x^2 − 2x) + 1", "input=9x^2 − 6x + 1"), + Iteration("(3x)^2 − 6x + 1!=9x^2 − 6x + 1", "answer=(3x)^2 − 6x + 1", "input=9x^2 − 6x + 1"), + Iteration("c(b-1)!=c*b-c", "answer=c(b-1)", "input=c*b-c"), + Iteration("x(x+4)+y!=x^2+y+4x", "answer=x(x+4)+y", "input=x^2+y+4x"), + Iteration("Y!=Y+5", "answer=Y", "input=Y+5"), + Iteration("5!=Y+5", "answer=5", "input=Y+5"), + Iteration("x+3!=x+1+2", "answer=x+3", "input=x+1+2"), + Iteration("(1 - 3x)^2!=(3x-1)^2", "answer=(1 - 3x)^2", "input=(3x-1)^2"), + Iteration("9x^2 - 6x - 1!=(3x-1)^2", "answer=9x^2 - 6x - 1", "input=(3x-1)^2"), + Iteration("(3x −1)!=(3x-1)^2", "answer=(3x −1)", "input=(3x-1)^2"), + Iteration("2x!=sqrt(2x)^2", "answer=2x", "input=sqrt(2x)^2"), + Iteration("2x!=sqrt(-4x^2)", "answer=2x", "input=sqrt(-4x^2)"), + Iteration("x^2+2x+1!=(x+2)^2", "answer=x^2+2x+1", "input=(x+2)^2"), + Iteration("x^2-1!=(x+1)(1-x)", "answer=x^2-1", "input=(x+1)(1-x)"), + Iteration("x+1!=(x^2+2x+1)/(x-1)", "answer=x+1", "input=(x^2+2x+1)/(x-1)"), + Iteration("x-1!=(x^2-1)/x", "answer=x-1", "input=(x^2-1)/x"), + Iteration("x+1!=(x^2-1)/(x-2)", "answer=x+1", "input=(x^2-1)/(x-2)"), + Iteration("-3x!=(9x^3)^(1/3)", "answer=-3x", "input=(9x^3)^(1/3)") + ) + fun testMatches_assortedExpressions_withoutMatchingCharacteristics_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // not pass for this classifier. + assertThat(matches).isFalse() + } + + private fun matchesClassifier( + answerExpression: InteractionObject, + inputExpression: InteractionObject, + allowedVariables: List = allPossibleVariables + ): Boolean { + return classifier.matches( + answerExpression, + inputs = mapOf("x" to inputExpression), + classificationContext = ClassificationContext( + customizationArgs = mapOf( + "customOskLetters" to SchemaObject.newBuilder().apply { + schemaObjectList = SchemaObjectList.newBuilder().apply { + addAllSchemaObject( + allowedVariables.map { + SchemaObject.newBuilder().setNormalizedString(it).build() + } + ) + }.build() + }.build() + ) + ) + ) + } + + private fun createMathExpression(rawExpression: String) = InteractionObject.newBuilder().apply { + mathExpression = rawExpression + }.build() + + private fun setUpTestApplicationComponent() { + DaggerAlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestDispatcherModule::class, LoggerModule::class, RobolectricModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(test: AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest) + } +} diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt new file mode 100644 index 00000000000..19dd3fbc92e --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt @@ -0,0 +1,666 @@ +package org.oppia.android.domain.classify.rules.algebraicexpressioninput + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner +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.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton +import org.oppia.android.app.model.SchemaObject +import org.oppia.android.app.model.SchemaObjectList +import org.oppia.android.domain.classify.ClassificationContext + +/** + * Tests for [AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider]. + * + * Note that the tests implemented in this suite are specifically set up to verify the cases + * outlined in this sheet: + * https://docs.google.com/spreadsheets/d/1u1fQdah2WsmdYKWKGmuXy5TPT7Ot-b8A7O9iZF-j5XE/edit#gid=0. + */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest { + @Inject internal lateinit var provider: AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider + + @Parameter lateinit var answer: String + @Parameter lateinit var input: String + + private lateinit var classifier: RuleClassifier + private val allPossibleVariables = (('a'..'z') + ('A'..'Z')).toList().map { it.toString() } + + @Before + fun setUp() { + setUpTestApplicationComponent() + classifier = provider.createRuleClassifier() + } + + @Test + fun testMatches_answerHasDisallowedVariable_returnsFalse() { + val answerExpression = createMathExpression("y") + val inputExpression = createMathExpression("y") + + val matches = matchesClassifier(answerExpression, inputExpression, allowedVariables = listOf()) + + // Despite the answer otherwise being equal, the variable isn't allowed. This shouldn't actually + // be the case in practice since neither the creator nor the learner would be allowed to input a + // disallowed variable (so this check is mainly a "just-in-case"). + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("0==0", "answer=0", "input=0"), + Iteration("1==1", "answer=1", "input=1"), + Iteration("2==2", "answer=2", "input=2"), + Iteration("x==x", "answer=x", "input=x"), + Iteration("y==y", "answer=y", "input=y") + ) + fun testMatches_sameSingleTerms_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("-2==-2", "answer=-2", "input=-2"), + Iteration("1+3.14==1+3.14", "answer=1+3.14", "input=1+3.14"), + Iteration(" 1 + 3.14 ==1+3.14", "answer= 1 + 3.14 ", "input=1+3.14"), + Iteration("1+2+3==1+2+3", "answer=1+2+3", "input=1+2+3"), + Iteration("1-3.14==1-3.14", "answer=1-3.14", "input=1-3.14"), + Iteration("2*3.14==2*3.14", "answer=2*3.14", "input=2*3.14"), + Iteration("2/3==2/3", "answer=2/3", "input=2/3"), + Iteration("2/3.14==2/3.14", "answer=2/3.14", "input=2/3.14"), + Iteration("2^3==2^3", "answer=2^3", "input=2^3"), + Iteration("2^3.14==2^3.14", "answer=2^3.14", "input=2^3.14"), + Iteration("sqrt(2)==sqrt(2)", "answer=sqrt(2)", "input=sqrt(2)"), + Iteration("-x==-x", "answer=-x", "input=-x"), + Iteration("x+3.14==x+3.14", "answer=x+3.14", "input=x+3.14"), + Iteration("x-3.14==x-3.14", "answer=x-3.14", "input=x-3.14"), + Iteration("x*3.14==x*3.14", "answer=x*3.14", "input=x*3.14"), + Iteration("x/3==x/3", "answer=x/3", "input=x/3"), + Iteration("sqrt(x)==sqrt(x)", "answer=sqrt(x)", "input=sqrt(x)") + ) + fun testMatches_sameSingleOperations_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("1!=0", "answer=1", "input=0"), + Iteration("0!=1", "answer=0", "input=1"), + Iteration("3.14!=1", "answer=3.14", "input=1"), + Iteration("1!=3.14", "answer=1", "input=3.14"), + Iteration("x!=3.14", "answer=x", "input=3.14"), + Iteration("y!=x", "answer=y", "input=x"), + Iteration("3.14!=x", "answer=3.14", "input=x") + ) + fun testMatches_differentSingleTerms_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions aren't exactly the same (minus whitespace and some minor term + // reordering), they won't match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("3.14+1==1+3.14", "answer=3.14+1", "input=1+3.14"), + Iteration("3+2+1==1+2+3", "answer=3+2+1", "input=1+2+3"), + Iteration("-3.14+1==1-3.14", "answer=-3.14+1", "input=1-3.14"), + Iteration("3.14*2==2*3.14", "answer=3.14*2", "input=2*3.14"), + Iteration("2+x==x+2", "answer=2+x", "input=x+2"), + Iteration("y+x==x+y", "answer=y+x", "input=x+y"), + Iteration("x*2==2x", "answer=x*2", "input=2x"), + Iteration("yx==xy", "answer=yx", "input=xy") + ) + fun testMatches_operationsDiffer_byCommutativity_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Reordering terms by commutativity is allowed by this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("1+(2+3)==(1+2)+3", "answer=1+(2+3)", "input=(1+2)+3"), + Iteration("2*(3*4)==(2*3)*4", "answer=2*(3*4)", "input=(2*3)*4"), + Iteration("x+(2+3)==(x+2)+3", "answer=x+(2+3)", "input=(x+2)+3"), + Iteration("x+(y+z)==(x+y)+z", "answer=x+(y+z)", "input=(x+y)+z"), + Iteration("2*(3x)==(2x)*3", "answer=2*(3x)", "input=(2x)*3"), + Iteration("x(yz)==(xy)z", "answer=x(yz)", "input=(xy)z") + ) + fun testMatches_operationsDiffer_byAssociativity_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Changing operation associativity is allowed by this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("3.14-1!=1-3.14", "answer=3.14-1", "input=1-3.14"), + Iteration("1-(2-3)!=(1-2)-3", "answer=1-(2-3)", "input=(1-2)-3"), + Iteration("3.14/2!=2/3.14", "answer=3.14/2", "input=2/3.14"), + Iteration("2/(3/4)!=(2/3)/4", "answer=2/(3/4)", "input=(2/3)/4"), + Iteration("3.14^2!=2^3.14", "answer=3.14^2", "input=2^3.14"), + Iteration("3.14-x!=x-3.14", "answer=3.14-x", "input=x-3.14"), + Iteration("x-(y-z)!=(x-y)-z", "answer=x-(y-z)", "input=(x-y)-z"), + Iteration("3.14/x!=x/3.14", "answer=3.14/x", "input=x/3.14"), + Iteration("x/(y/z)!=(x/y)/z", "answer=x/(y/z)", "input=(x/y)/z") + ) + fun testMatches_operationsDiffer_byNonCommutativeOrAssociativeReordering_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Non-commutative and non-associative reordering generally results in a different value, so the + // classifier will fail to match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("1+2==1-(-2)", "answer=1+2", "input=1-(-2)"), + Iteration("1+x==1-(-x)", "answer=1+x", "input=1-(-x)") + ) + fun testMatches_operationsDiffer_byDistributingNegation_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // The classifier does support distributing negations (e.g. a*cross groups). + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("x-y==-(y-x)", "answer=x-y", "input=-(y-x)"), + Iteration("1-2!=-(2-1)", "answer=1-2", "input=-(2-1)"), + Iteration("1+2!=1+1+1", "answer=1+2", "input=1+1+1"), + Iteration("4-6!=1-2-1", "answer=4-6", "input=1-2-1"), + Iteration("2*3*2*2!=2*3*4", "answer=2*3*2*2", "input=2*3*4"), + Iteration("-6-2!=2*-(3+1)", "answer=-6-2", "input=2*-(3+1)"), + Iteration("2/3/2/2!=2/3/4", "answer=2/3/2/2", "input=2/3/4"), + Iteration("2^(2+1)!=2^3", "answer=2^(2+1)", "input=2^3"), + Iteration("2^(-1)!=1/2", "answer=2^(-1)", "input=1/2"), + Iteration("2+x!=1+x+1", "answer=2+x", "input=1+x+1"), + Iteration("x!=1-x-1", "answer=x", "input=1-x-1"), + Iteration("4x!=2*2*x", "answer=4x", "input=2*2*x"), + Iteration("2-6x!=2*(-3x+1)", "answer=2-6x", "input=2*(-3x+1)"), + Iteration("x/4!=x/2/2", "answer=x/4", "input=x/2/2"), + Iteration("x^(2+1)!=x^3", "answer=x^(2+1)", "input=x^3"), + Iteration("x*(2^(-1))!=x/2", "answer=x*(2^(-1))", "input=x/2") + ) + fun testMatches_operationsDiffer_byDistributionAndCombining_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier doesn't support broadly distributing or combining terms. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("2*(2+6+3+4)==2*(2+6+3+4)", "answer=2*(2+6+3+4)", "input=2*(2+6+3+4)"), + Iteration("2 × (2+6+3+4)==2*(2+6+3+4)", "answer=2 × (2+6+3+4)", "input=2*(2+6+3+4)"), + Iteration( + "15 - (6 × 2) + 3==15 - (6 × 2) + 3", "answer=15 - (6 × 2) + 3", "input=15 - (6 × 2) + 3" + ), + Iteration( + "2 × (50 + 150 + 100 + 25) ==(50 + 150 + 100 + 25) × 2", + "answer=2 × (50 + 150 + 100 + 25) ", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration( + "2 * (50 + 150 + 100 + 25) ==2 × (50 + 150 + 100 + 25)", + "answer=2 * (50 + 150 + 100 + 25) ", + "input=2 × (50 + 150 + 100 + 25)" + ), + Iteration("2+5==5+2", "answer=2+5", "input=5+2"), + Iteration("5+2==5+2", "answer=5+2", "input=5+2"), + Iteration("6 + 4!=6 − (− 4)", "answer=6 + 4", "input=6 − (− 4)"), + Iteration("6 − (− 4)==6 − (− 4)", "answer=6 − (− 4)", "input=6 − (− 4)"), + Iteration("6-(-4)==6 − (− 4)", "answer=6-(-4)", "input=6 − (− 4)"), + Iteration("− (− 4) + 6==6 − (− 4)", "answer=− (− 4) + 6", "input=6 − (− 4)"), + Iteration("10^−5 * 3!=3 * 10^-5", "answer=10^−5 * 3", "input=3 * 10^-5"), + Iteration("3 * 10^-5==3 * 10^-5", "answer=3 * 10^-5", "input=3 * 10^-5"), + Iteration( + "1000 + 200 + 30 + 4 + 0.5 + 0.06==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "200 + 30 + 4 + 0.5 + 0.06 + 1000==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=200 + 30 + 4 + 0.5 + 0.06 + 1000", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "0.06 + 0.5 + 4 + 30 + 200 + 1000==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=0.06 + 0.5 + 4 + 30 + 200 + 1000", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration("2 * 2 * 3 * 3==2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration("4x^2+20x==4*x^2+20x", "answer=4x^2+20x", "input=4*x^2+20x"), + Iteration("3+x-5==3+x-5", "answer=3+x-5", "input=3+x-5"), + Iteration("Z+A-Z==Z+A-Z", "answer=Z+A-Z", "input=Z+A-Z"), + Iteration("6C - 5A -1==6C - 5A -1", "answer=6C - 5A -1", "input=6C - 5A -1"), + Iteration("5Z-w==5*Z-w", "answer=5Z-w", "input=5*Z-w"), + Iteration("5*Z-w==5*Z-w", "answer=5*Z-w", "input=5*Z-w"), + Iteration("LS-3S+L==L*S-3S+L", "answer=LS-3S+L", "input=L*S-3S+L"), + Iteration("L*S-3S+L==L*S-3S+L", "answer=L*S-3S+L", "input=L*S-3S+L"), + Iteration("L*S-3*S+L==L*S-3S+L", "answer=L*S-3*S+L", "input=L*S-3S+L"), + Iteration("LS-3*S+L==L*S-3S+L", "answer=LS-3*S+L", "input=L*S-3S+L"), + Iteration("9x^2 − 6x + 1==9x^2 − 6x + 1", "answer=9x^2 − 6x + 1", "input=9x^2 − 6x + 1"), + Iteration("c*b-c==c*b-c", "answer=c*b-c", "input=c*b-c"), + Iteration("bc-c==c*b-c", "answer=bc-c", "input=c*b-c"), + Iteration("cb-c==c*b-c", "answer=cb-c", "input=c*b-c"), + Iteration("-c+bc==c*b-c", "answer=-c+bc", "input=c*b-c"), + Iteration("-c+cb==c*b-c", "answer=-c+cb", "input=c*b-c"), + Iteration("x^2+y+4x==x^2+y+4x", "answer=x^2+y+4x", "input=x^2+y+4x"), + Iteration("y+4x+x^2==x^2+y+4x", "answer=y+4x+x^2", "input=x^2+y+4x"), + Iteration("x^2+4x+y==x^2+y+4x", "answer=x^2+4x+y", "input=x^2+y+4x"), + Iteration("Y+5==Y+5", "answer=Y+5", "input=Y+5"), + Iteration("5+Y==Y+5", "answer=5+Y", "input=Y+5"), + Iteration( + "a^2 + b^2 + c^2+ 2a*b + 2a*c + 2bc==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2+ 2a*b + 2a*c + 2bc", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a^2 + b^2 + c^2+ 2a*b + 2bc + 2a*c==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2+ 2a*b + 2bc + 2a*c", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "2a*b + 2bc + 2a*c + a^2 + b^2 + c^2==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=2a*b + 2bc + 2a*c + a^2 + b^2 + c^2", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "2a*b + b^2 + c^2+ a^2 + 2bc + 2a*c==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=2a*b + b^2 + c^2+ a^2 + 2bc + 2a*c", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration("1 - 6x + 9x^2==9x^2 − 6x + 1", "answer=1 - 6x + 9x^2", "input=9x^2 − 6x + 1"), + Iteration("9x^2 + 1 - 6x==9x^2 − 6x + 1", "answer=9x^2 + 1 - 6x", "input=9x^2 − 6x + 1"), + Iteration("2+1+x==x+1+2", "answer=2+1+x", "input=x+1+2"), + Iteration("1+2+x==x+1+2", "answer=1+2+x", "input=x+1+2"), + Iteration("1+x+2==x+1+2", "answer=1+x+2", "input=x+1+2"), + Iteration("2+x+1==x+1+2", "answer=2+x+1", "input=x+1+2"), + Iteration("(x+1)+2==x+1+2", "answer=(x+1)+2", "input=x+1+2"), + Iteration("x + (1+2)==x+1+2", "answer=x + (1+2)", "input=x+1+2"), + Iteration( + "y+1+ 9x(x − 6)==9x(x − 6) + 1+ y", "answer=y+1+ 9x(x − 6)", "input=9x(x − 6) + 1+ y" + ), + Iteration("1+y+9x(x − 6)==9x(x − 6) + 1+ y", "answer=1+y+9x(x − 6)", "input=9x(x − 6) + 1+ y"), + Iteration( + "1 + 9x(x − 6) + y==9x(x − 6) + 1+ y", "answer=1 + 9x(x − 6) + y", "input=9x(x − 6) + 1+ y" + ), + Iteration( + "(y+1)+9x(x − 6)==9x(x − 6) + 1+ y", "answer=(y+1)+9x(x − 6)", "input=9x(x − 6) + 1+ y" + ), + Iteration( + "(x^2 − x)/3 − 4y==(x^2 − x)/3 − 4y", "answer=(x^2 − x)/3 − 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "-4y + (x^2 − x)/3==(x^2 − x)/3 − 4y", "answer=-4y + (x^2 − x)/3", "input=(x^2 − x)/3 − 4y" + ), + Iteration("(3x -1)^2==(3x-1)^2", "answer=(3x -1)^2", "input=(3x-1)^2"), + Iteration("(2+6+3+4)*2==2*(2+6+3+4)", "answer=(2+6+3+4)*2", "input=2*(2+6+3+4)"), + Iteration("(2+6+3+4) × 2==2*(2+6+3+4)", "answer=(2+6+3+4) × 2", "input=2*(2+6+3+4)"), + Iteration( + "3 - (6 * 2) + 15==15 - (6 × 2) + 3", "answer=3 - (6 * 2) + 15", "input=15 - (6 × 2) + 3" + ), + Iteration( + "15 - (2 × 6) + 3==15 - (6 × 2) + 3", "answer=15 - (2 × 6) + 3", "input=15 - (6 × 2) + 3" + ), + Iteration( + "2* ( 25+50+100+150)==(50 + 150 + 100 + 25) × 2", + "answer=2* ( 25+50+100+150)", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration("20x+4x^2==4*x^2+20x", "answer=20x+4x^2", "input=4*x^2+20x"), + Iteration("x-5+3==3+x-5", "answer=x-5+3", "input=3+x-5"), + Iteration("-5+3+x==3+x-5", "answer=-5+3+x", "input=3+x-5"), + Iteration("-5+x+3==3+x-5", "answer=-5+x+3", "input=3+x-5"), + Iteration("3+(x-5)==3+x-5", "answer=3+(x-5)", "input=3+x-5"), + Iteration("A+Z-Z==Z+A-Z", "answer=A+Z-Z", "input=Z+A-Z"), + Iteration("Z+(A-Z)==Z+A-Z", "answer=Z+(A-Z)", "input=Z+A-Z"), + Iteration("6C - (5A+1)==6C - 5A -1", "answer=6C - (5A+1)", "input=6C - 5A -1"), + Iteration("-5A-1+6C==6C - 5A -1", "answer=-5A-1+6C", "input=6C - 5A -1"), + Iteration("-W+5Z==5*Z-W", "answer=-W+5Z", "input=5*Z-W"), + Iteration("L+LS-3S==L*S-3S+L", "answer=L+LS-3S", "input=L*S-3S+L"), + Iteration( + "- 4y + (x^2 − x)/3==(x^2 − x)/3 − 4y", "answer=- 4y + (x^2 − x)/3", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "a^2+ b^2 + c^2 + 2bc + 2a*c + 2a*b==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2+ b^2 + c^2 + 2bc + 2a*c + 2a*b", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ) + ) + fun testMatches_assortedExpressions_withMatchingCharacteristics_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // pass for this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("10!=6 − (− 4)", "answer=10", "input=6 − (− 4)"), + Iteration("6 + 2^2!=6 − (− 4)", "answer=6 + 2^2", "input=6 − (− 4)"), + Iteration("3 * 2 − (− 4)!=6 − (− 4)", "answer=3 * 2 − (− 4)", "input=6 − (− 4)"), + Iteration("100/10!=6 − (− 4)", "answer=100/10", "input=6 − (− 4)"), + Iteration("3/(10 * 10^4)!=3 * 10^-5", "answer=3/(10 * 10^4)", "input=3 * 10^-5"), + Iteration( + "1234.56!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1234.56", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "123456/100!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=123456/100", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "61728/50!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=61728/50", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1234 + 56/100!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1234 + 56/100", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1230 + 4.56!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1230 + 4.56", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "2 * 2 * 3 * 3 * 1!=2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3 * 1", "input=2 * 2 * 3 * 3" + ), + Iteration("2 * 2 * 9!=2 * 2 * 3 * 3", "answer=2 * 2 * 9", "input=2 * 2 * 3 * 3"), + Iteration("4 * 3^2!=2 * 2 * 3 * 3", "answer=4 * 3^2", "input=2 * 2 * 3 * 3"), + Iteration("8/2 * 3 * 3!=2 * 2 * 3 * 3", "answer=8/2 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration("36!=2 * 2 * 3 * 3", "answer=36", "input=2 * 2 * 3 * 3"), + Iteration("sqrt(4-2)!=sqrt(2)", "answer=sqrt(4-2)", "input=sqrt(2)"), + Iteration( + "(a+ b)^2 + c^2 + 2bc + 2a*c!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=(a+ b)^2 + c^2 + 2bc + 2a*c", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(a+b+c)^2!=a^2+b^2+c^2+2a*b+2a*c+2bc", "answer=(a+b+c)^2", "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(-a -b -c)^2!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=(-a -b -c)^2", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "x(x − 1)/3 −4y!=(x^2 − x)/3 − 4y", "answer=x(x − 1)/3 −4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "x^2/3 − x/3 − 4y!=(x^2 − x)/3 − 4y", "answer=x^2/3 − x/3 − 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "x^2/3 − (x/3 + 4y)!=(x^2 − x)/3 − 4y", "answer=x^2/3 − (x/3 + 4y)", "input=(x^2 − x)/3 − 4y" + ), + Iteration("√(3x −1)4!=(3x-1)^2", "answer=√(3x −1)4", "input=(3x-1)^2"), + Iteration("3x(3x - 2) + 1!=(3x-1)^2", "answer=3x(3x - 2) + 1", "input=(3x-1)^2"), + Iteration("3(3x^2) - 6x +1!=(3x-1)^2", "answer=3(3x^2) - 6x +1", "input=(3x-1)^2"), + Iteration("2x!=sqrt(4x^2)", "answer=2x", "input=sqrt(4x^2)"), + Iteration("x^2+2x+1!=(x+1)^2", "answer=x^2+2x+1", "input=(x+1)^2"), + Iteration("x^2-1!=(x+1)(x-1)", "answer=x^2-1", "input=(x+1)(x-1)"), + Iteration("x+1!=(x^2+2x+1)/(x+1)", "answer=x+1", "input=(x^2+2x+1)/(x+1)"), + Iteration("x-1!=(x^2-1)/(x+1)", "answer=x-1", "input=(x^2-1)/(x+1)"), + Iteration("x+1!=(x^2-1)/(x-1)", "answer=x+1", "input=(x^2-1)/(x-1)"), + Iteration("-3x!=(-27x^3)^(1/3)", "answer=-3x", "input=(-27x^3)^(1/3)"), + Iteration("1!=(x^2-1)/(x^2-1)", "answer=1", "input=(x^2-1)/(x^2-1)"), + Iteration("2*(6+3+4) + 4!=2*(2+6+3+4)", "answer=2*(6+3+4) + 4", "input=2*(2+6+3+4)"), + Iteration("2*(2+6+3) + 8!=2*(2+6+3+4)", "answer=2*(2+6+3) + 8", "input=2*(2+6+3+4)"), + Iteration("15 - 12 + 3!=15 - (6 × 2) + 3", "answer=15 - 12 + 3", "input=15 - (6 × 2) + 3"), + Iteration( + "2 *(50 + 150) + 2*(100 + 25)!=(50 + 150 + 100 + 25) × 2", + "answer=2 *(50 + 150) + 2*(100 + 25)", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration("3 * 10^5!=3 * 10^-5", "answer=3 * 10^5", "input=3 * 10^-5"), + Iteration("2 * 10^−5!=3 * 10^-5", "answer=2 * 10^−5", "input=3 * 10^-5"), + Iteration("5 * 10^−3!=3 * 10^-5", "answer=5 * 10^−3", "input=3 * 10^-5"), + Iteration("30 * 10^−6!=3 * 10^-5", "answer=30 * 10^−6", "input=3 * 10^-5"), + Iteration("0.00003!=3 * 10^-5", "answer=0.00003", "input=3 * 10^-5"), + Iteration("3/10^5!=3 * 10^-5", "answer=3/10^5", "input=3 * 10^-5"), + Iteration( + "123456!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=123456", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1000 + 200 + 30!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1000 + 200 + 30", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration("3 *2 – (− 4)!=6 − (− 4)", "answer=3 *2 – (− 4)", "input=6 − (− 4)"), + Iteration("6 − 4!=6 − (− 4)", "answer=6 − 4", "input=6 − (− 4)"), + Iteration("6 + (− 4)!=6 − (− 4)", "answer=6 + (− 4)", "input=6 − (− 4)"), + Iteration("100!=6 − (− 4)", "answer=100", "input=6 − (− 4)"), + Iteration("7!=5+2", "answer=7", "input=5+2"), + Iteration("3+4!=5+2", "answer=3+4", "input=5+2"), + Iteration("2 * 2 * 3!=2 * 2 * 3 * 3", "answer=2 * 2 * 3", "input=2 * 2 * 3 * 3"), + Iteration("2 * 3 * 3 * 3!=2 * 2 * 3 * 3", "answer=2 * 3 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration("A!=Z+A-Z", "answer=A", "input=Z+A-Z"), + Iteration("L(1+S)-3S!=L*S-3S+L", "answer=L(1+S)-3S", "input=L*S-3S+L"), + Iteration("S(L-3)+L!=L*S-3S+L", "answer=S(L-3)+L", "input=L*S-3S+L"), + Iteration( + "x(x − 1)/3 − 4y!=(x^2 − x)/3 − 4y", "answer=x(x − 1)/3 − 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(x^2 − x) * 3^-1 − 4y!=(x^2 − x)/3 − 4y", + "answer=(x^2 − x) * 3^-1 − 4y", + "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "x(x^2 − x)/3 − 4y!=(x^2 − x)/3 − 4y", "answer=x(x^2 − x)/3 − 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(x^2 − x)/3 + 4y!=(x^2 − x)/3 − 4y", "answer=(x^2 − x)/3 + 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(x^2 + x)/3 - 4y!=(x^2 − x)/3 − 4y", "answer=(x^2 + x)/3 - 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(x^2 − x)*0.33 - 4y!=(x^2 − x)/3 − 4y", + "answer=(x^2 − x)*0.33 - 4y", + "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "a*a + b*b + c*c + 2*a*b + 2*a*c + 2*b*c==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a*a + b*b + c*c + 2*a*b + 2*a*c + 2*b*c", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a^2 + b^2 + c^2 + 2(a*b + a*c + bc)!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2 + 2(a*b + a*c + bc)", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(a + b)^2 + c^2 + 2a*c + 2bc!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=(a + b)^2 + c^2 + 2a*c + 2bc", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a * a + b * b + c^3/c + 2a*b + 2a*c + 2bc!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a * a + b * b + c^3/c + 2a*b + 2a*c + 2bc", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(a + b + c)^3!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=(a + b + c)^3", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a^2 + b^2 + c^2!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a^2 + b^2 + c^2- 2a*b - 2a*c - 2bc!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2- 2a*b - 2a*c - 2bc", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration("(3x − 1)^2!=9x^2 − 6x + 1", "answer=(3x − 1)^2", "input=9x^2 − 6x + 1"), + Iteration("3x(3x − 2) + 1!=9x^2 − 6x + 1", "answer=3x(3x − 2) + 1", "input=9x^2 − 6x + 1"), + Iteration("3(3x^2 − 2x) + 1!=9x^2 − 6x + 1", "answer=3(3x^2 − 2x) + 1", "input=9x^2 − 6x + 1"), + Iteration("(3x)^2 − 6x + 1!=9x^2 − 6x + 1", "answer=(3x)^2 − 6x + 1", "input=9x^2 − 6x + 1"), + Iteration("c(b-1)!=c*b-c", "answer=c(b-1)", "input=c*b-c"), + Iteration("x(x+4)+y!=x^2+y+4x", "answer=x(x+4)+y", "input=x^2+y+4x"), + Iteration("Y!=Y+5", "answer=Y", "input=Y+5"), + Iteration("5!=Y+5", "answer=5", "input=Y+5"), + Iteration("x+3!=x+1+2", "answer=x+3", "input=x+1+2"), + Iteration("(1 - 3x)^2!=(3x-1)^2", "answer=(1 - 3x)^2", "input=(3x-1)^2"), + Iteration("9x^2 - 6x - 1!=(3x-1)^2", "answer=9x^2 - 6x - 1", "input=(3x-1)^2"), + Iteration("(3x −1)!=(3x-1)^2", "answer=(3x −1)", "input=(3x-1)^2"), + Iteration("2x!=sqrt(2x)^2", "answer=2x", "input=sqrt(2x)^2"), + Iteration("2x!=sqrt(-4x^2)", "answer=2x", "input=sqrt(-4x^2)"), + Iteration("x^2+2x+1!=(x+2)^2", "answer=x^2+2x+1", "input=(x+2)^2"), + Iteration("x^2-1!=(x+1)(1-x)", "answer=x^2-1", "input=(x+1)(1-x)"), + Iteration("x+1!=(x^2+2x+1)/(x-1)", "answer=x+1", "input=(x^2+2x+1)/(x-1)"), + Iteration("x-1!=(x^2-1)/x", "answer=x-1", "input=(x^2-1)/x"), + Iteration("x+1!=(x^2-1)/(x-2)", "answer=x+1", "input=(x^2-1)/(x-2)"), + Iteration("-3x!=(9x^3)^(1/3)", "answer=-3x", "input=(9x^3)^(1/3)") + ) + fun testMatches_assortedExpressions_withoutMatchingCharacteristics_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // not pass for this classifier. + assertThat(matches).isFalse() + } + + private fun matchesClassifier( + answerExpression: InteractionObject, + inputExpression: InteractionObject, + allowedVariables: List = allPossibleVariables + ): Boolean { + return classifier.matches( + answerExpression, + inputs = mapOf("x" to inputExpression), + classificationContext = ClassificationContext( + customizationArgs = mapOf( + "customOskLetters" to SchemaObject.newBuilder().apply { + schemaObjectList = SchemaObjectList.newBuilder().apply { + addAllSchemaObject( + allowedVariables.map { + SchemaObject.newBuilder().setNormalizedString(it).build() + } + ) + }.build() + }.build() + ) + ) + ) + } + + private fun createMathExpression(rawExpression: String) = InteractionObject.newBuilder().apply { + mathExpression = rawExpression + }.build() + + private fun setUpTestApplicationComponent() { + DaggerAlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestDispatcherModule::class, LoggerModule::class, RobolectricModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject( + test: AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest + ) + } +} diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModuleTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModuleTest.kt new file mode 100644 index 00000000000..92ebc07cc5d --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModuleTest.kt @@ -0,0 +1,107 @@ +package org.oppia.android.domain.classify.rules.algebraicexpressioninput + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.domain.classify.RuleClassifier +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.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton +import org.oppia.android.domain.classify.rules.AlgebraicExpressionInputRules + +/** Tests for [AlgebraicExpressionInputModule]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class AlgebraicExpressionInputModuleTest { + @Inject + @AlgebraicExpressionInputRules + lateinit var algebraicExpressionInputClassifiers: Map< + String, @JvmSuppressWildcards RuleClassifier> + + @Before + fun setUp() { + setUpTestApplicationComponent() + } + + @Test + fun testModule_hasAtLeastOneClassifier() { + assertThat(algebraicExpressionInputClassifiers).isNotEmpty() + } + + @Test + fun testModule_hasNoDuplicateClassifiers() { + assertThat(algebraicExpressionInputClassifiers.values.toSet()).hasSize( + algebraicExpressionInputClassifiers.size + ) + } + + @Test + fun testModule_providesMatchesExactlyWithClassifier() { + assertThat(algebraicExpressionInputClassifiers).containsKey("MatchesExactlyWith") + } + + @Test + fun testModule_providesMatchesUpToTrivialManipulationsClassifier() { + assertThat(algebraicExpressionInputClassifiers).containsKey("MatchesUpToTrivialManipulations") + } + + @Test + fun testModule_providesIsEquivalentToClassifier() { + assertThat(algebraicExpressionInputClassifiers).containsKey("MatchesExactlyWith") + } + + private fun setUpTestApplicationComponent() { + DaggerAlgebraicExpressionInputModuleTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestDispatcherModule::class, LoggerModule::class, RobolectricModule::class, + AlgebraicExpressionInputModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(test: AlgebraicExpressionInputModuleTest) + } +} diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/BUILD.bazel new file mode 100644 index 00000000000..a9bacbd93ba --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/BUILD.bazel @@ -0,0 +1,105 @@ +""" +Tests for algebraic expression input classifiers. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("//:oppia_android_test.bzl", "oppia_android_test") + +oppia_android_test( + name = "AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest", + srcs = ["AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt"], + custom_package = "org.oppia.android.domain.classify.rules.algebraicexpressioninput", + test_class = "org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_core", + "//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/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + +oppia_android_test( + name = "AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest", + srcs = ["AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt"], + custom_package = "org.oppia.android.domain.classify.rules.algebraicexpressioninput", + test_class = "org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_core", + "//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/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + +oppia_android_test( + name = "AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest", + srcs = ["AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt"], + custom_package = "org.oppia.android.domain.classify.rules.algebraicexpressioninput", + test_class = "org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_core", + "//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/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + +oppia_android_test( + name = "AlgebraicExpressionInputModuleTest", + srcs = ["AlgebraicExpressionInputModuleTest.kt"], + custom_package = "org.oppia.android.domain.classify.rules.algebraicexpressioninput", + test_class = "org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModuleTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//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/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + +dagger_rules() diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt index beef3fdf622..a9af6bd65fb 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt @@ -34,11 +34,11 @@ import org.oppia.android.domain.classify.rules.numericexpressioninput.DaggerNume import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider as RuleClassifierProvider /** - * Tests for [RuleClassifierProvider]. + * Tests for [NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider]. * * Note that the tests implemented in this suite are specifically set up to verify the cases * outlined in this sheet: - * https://docs.google.com/spreadsheets/dNumericExpressionInputIsEquivalentToRuleClassifierProvider/1u1fQdah2WsmdYKWKGmuXy5TPT7Ot-b8A7O9iZF-j5XE/edit#gid=0. + * https://docs.google.com/spreadsheets/d/1u1fQdah2WsmdYKWKGmuXy5TPT7Ot-b8A7O9iZF-j5XE/edit#gid=0. */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") diff --git a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt index b3cc704c4c9..9711b1d0c87 100644 --- a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt @@ -3725,7 +3725,7 @@ class PolynomialExtensionsTest { val result = polynomial1 pow polynomial2 - // (-9x^3)^(1/3)=-3x (demonstrates real number rooting, i.e. support for negative coefficients + // (-27x^3)^(1/3)=-3x (demonstrates real number rooting, i.e. support for negative coefficients // in certain cases). assertThat(result).apply { hasTermCountThat().isEqualTo(1) From 0a7cb6945f563f9b6fa4c64ae8a7438ceb6266c3 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 22:54:34 -0800 Subject: [PATCH 110/134] Lint & static check fixes. --- .../domain/classify/ClassificationContext.kt | 9 +++++++++ ...onInputIsEquivalentToRuleClassifierProvider.kt | 10 +++++++++- ...putMatchesExactlyWithRuleClassifierProvider.kt | 9 ++++++++- ...oTrivialManipulationsRuleClassifierProvider.kt | 11 ++++++++++- ...onInputIsEquivalentToRuleClassifierProvider.kt | 2 +- ...putIsEquivalentToRuleClassifierProviderTest.kt | 7 +++---- ...atchesExactlyWithRuleClassifierProviderTest.kt | 14 ++++++++------ ...vialManipulationsRuleClassifierProviderTest.kt | 15 ++++++++------- .../AlgebraicExpressionInputModuleTest.kt | 2 +- ...putIsEquivalentToRuleClassifierProviderTest.kt | 3 +-- ...atchesExactlyWithRuleClassifierProviderTest.kt | 3 +-- ...vialManipulationsRuleClassifierProviderTest.kt | 3 +-- .../file_content_validation_checks.textproto | 3 +++ scripts/assets/test_file_exemptions.textproto | 1 + 14 files changed, 64 insertions(+), 28 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/ClassificationContext.kt b/domain/src/main/java/org/oppia/android/domain/classify/ClassificationContext.kt index 01415330ce2..5d2ba3b88be 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/ClassificationContext.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/ClassificationContext.kt @@ -3,6 +3,15 @@ package org.oppia.android.domain.classify import org.oppia.android.app.model.SchemaObject import org.oppia.android.app.model.WrittenTranslationContext +/** + * Represents the context provided to classifiers when they're classifying an answer. + * + * This object provides context for the interaction and learner settings to help classifiers + * properly categorize and process answers. + * + * @property writtenTranslationContext the [WrittenTranslationContext] currently used by the learner + * @property customizationArgs the customization arguments defined by the current interaction + */ data class ClassificationContext( val writtenTranslationContext: WrittenTranslationContext = WrittenTranslationContext.getDefaultInstance(), diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt index 56e771b93c0..fb4f9bf3c8a 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -9,10 +9,18 @@ import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression +import org.oppia.android.util.math.isApproximatelyEqualTo import org.oppia.android.util.math.toPolynomial import javax.inject.Inject -import org.oppia.android.util.math.isApproximatelyEqualTo +/** + * Provider for a classifier that determines whether an algebraic expression is mathematically + * equivalent to the creator-specific expression defined as the input to this interaction. + * + * Note that both expressions are assumed and parsed as polynomials. + * + * See this class's tests for a list of supported cases (both for matching and not matching). + */ class AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, private val consoleLogger: ConsoleLogger diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt index 4c20ce52903..c35ba68b7f2 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -9,9 +9,16 @@ import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression -import javax.inject.Inject import org.oppia.android.util.math.isApproximatelyEqualTo +import javax.inject.Inject +/** + * Provider for a classifier that determines whether an algebraic expression is exactly equal to the + * creator-specific expression defined as the input to this interaction, including any parenthetical + * groups in the expressions and operand order. + * + * See this class's tests for a list of supported cases (both for matching and not matching). + */ class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, private val consoleLogger: ConsoleLogger diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index 6f618dfc857..27a02bfdd54 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -1,6 +1,5 @@ package org.oppia.android.domain.classify.rules.algebraicexpressioninput -import javax.inject.Inject import org.oppia.android.app.model.ComparableOperation import org.oppia.android.app.model.InteractionObject import org.oppia.android.domain.classify.ClassificationContext @@ -12,7 +11,17 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingRes import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression import org.oppia.android.util.math.isApproximatelyEqualTo import org.oppia.android.util.math.toComparableOperation +import javax.inject.Inject +/** + * Provider for a classifier that determines whether an algebraic expression is equal to the + * creator-specific expression defined as the input to this interaction, with some manipulations. + * + * 'Trivial manipulations' indicates rearranging any operands for commutative operations, or changes + * in resolution order (i.e. associative) without changing the meaning of the expression. + * + * See this class's tests for a list of supported cases (both for matching and not matching). + */ class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt index 54202d71aed..058a7bc6e96 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -1,6 +1,5 @@ package org.oppia.android.domain.classify.rules.numericexpressioninput -import javax.inject.Inject import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.Real import org.oppia.android.domain.classify.ClassificationContext @@ -12,6 +11,7 @@ import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.evaluateAsNumericExpression import org.oppia.android.util.math.isApproximatelyEqualTo +import javax.inject.Inject /** * Provider for a classifier that determines whether a numeric expression is numerically equivalent diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt index 60d09fbf36b..c64a5fc7640 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt @@ -12,7 +12,9 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.app.model.SchemaObject +import org.oppia.android.app.model.SchemaObjectList +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration @@ -29,9 +31,6 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.app.model.SchemaObject -import org.oppia.android.app.model.SchemaObjectList -import org.oppia.android.domain.classify.ClassificationContext /** * Tests for [AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider]. diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt index 0c6e401a976..0511f4c6d0a 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt @@ -12,7 +12,9 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.app.model.SchemaObject +import org.oppia.android.app.model.SchemaObjectList +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration @@ -29,9 +31,7 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.app.model.SchemaObject -import org.oppia.android.app.model.SchemaObjectList -import org.oppia.android.domain.classify.ClassificationContext +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.DaggerAlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest_TestApplicationComponent as DaggerTestApplicationComponent /** * Tests for [AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider]. @@ -388,7 +388,9 @@ class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest { "(a+b+c)^2!=a^2+b^2+c^2+2a*b+2a*c+2bc", "answer=(a+b+c)^2", "input=a^2+b^2+c^2+2a*b+2a*c+2bc" ), Iteration( - "(-a -b -c)^2!=a^2+b^2+c^2+2a*b+2a*c+2bc", "answer=(-a -b -c)^2", "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + "(-a -b -c)^2!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=(-a -b -c)^2", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" ), Iteration("1 - 6x + 9x^2!=9x^2 − 6x + 1", "answer=1 - 6x + 9x^2", "input=9x^2 − 6x + 1"), Iteration("9x^2 + 1 - 6x!=9x^2 − 6x + 1", "answer=9x^2 + 1 - 6x", "input=9x^2 − 6x + 1"), @@ -612,7 +614,7 @@ class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest { }.build() private fun setUpTestApplicationComponent() { - DaggerAlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest_TestApplicationComponent + DaggerTestApplicationComponent .builder() .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt index 19dd3fbc92e..22e3f4b749f 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt @@ -12,7 +12,9 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.app.model.SchemaObject +import org.oppia.android.app.model.SchemaObjectList +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration @@ -29,12 +31,11 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.app.model.SchemaObject -import org.oppia.android.app.model.SchemaObjectList -import org.oppia.android.domain.classify.ClassificationContext +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider as RuleClassifierProvider +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.DaggerAlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent as DaggerTestApplicationComponent /** - * Tests for [AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider]. + * Tests for [RuleClassifierProvider]. * * Note that the tests implemented in this suite are specifically set up to verify the cases * outlined in this sheet: @@ -47,7 +48,7 @@ import org.oppia.android.domain.classify.ClassificationContext @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest { - @Inject internal lateinit var provider: AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider + @Inject internal lateinit var provider: RuleClassifierProvider @Parameter lateinit var answer: String @Parameter lateinit var input: String @@ -627,7 +628,7 @@ class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvi }.build() private fun setUpTestApplicationComponent() { - DaggerAlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent + DaggerTestApplicationComponent .builder() .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModuleTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModuleTest.kt index 92ebc07cc5d..9d71d9b2a91 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModuleTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModuleTest.kt @@ -13,6 +13,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.AlgebraicExpressionInputRules import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule @@ -22,7 +23,6 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.domain.classify.rules.AlgebraicExpressionInputRules /** Tests for [AlgebraicExpressionInputModule]. */ // FunctionName: test names are conventionally named with underscores. diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt index 4ef5e2928c4..611fcb04b04 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt @@ -12,7 +12,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration @@ -29,7 +29,6 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.domain.classify.ClassificationContext /** * Tests for [NumericExpressionInputIsEquivalentToRuleClassifierProvider]. diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt index 228e1e3582e..b091adab625 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt @@ -12,7 +12,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration @@ -29,7 +29,6 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.rules.numericexpressioninput.DaggerNumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest_TestApplicationComponent as DaggerTestApplicationComponent /** diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt index a9af6bd65fb..9e095d504dd 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt @@ -12,7 +12,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration @@ -29,7 +29,6 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.rules.numericexpressioninput.DaggerNumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent as DaggerTestApplicationComponent import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider as RuleClassifierProvider diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index 1408521c618..03c575af12c 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -286,6 +286,9 @@ file_content_checks { file_path_regex: ".+?\\.kt" prohibited_content_regex: "OppiaParameterizedTestRunner" failure_message: "To use OppiaParameterizedTestRunner, please add an exemption to file_content_validation_checks.textproto and add an explanation for your use case in your PR description. Note that parameterized tests should only be used in special circumstances where a single behavior can be tested across multiple inputs, or for especially large test suites that can be trivially reduced." + exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt" + exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt" + exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt" diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index c44fadf2533..6b58b289fd9 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -550,6 +550,7 @@ exempted_file_path: "data/src/main/java/org/oppia/android/data/backends/gae/mode exempted_file_path: "data/src/main/java/org/oppia/android/data/backends/gae/model/GaeVoiceover.kt" exempted_file_path: "data/src/main/java/org/oppia/android/data/backends/gae/model/GaeWrittenTranslation.kt" exempted_file_path: "data/src/main/java/org/oppia/android/data/backends/gae/model/GaeWrittenTranslations.kt" +exempted_file_path: "domain/src/main/java/org/oppia/android/domain/classify/ClassificationContext.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/classify/ClassificationResult.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/classify/GenericInteractionClassifier.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/classify/InteractionClassifier.kt" From fd0c0cc51a98ee6c09c82962189dd56c6baebfb4 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 23:12:16 -0800 Subject: [PATCH 111/134] Fix test on Gradle. --- .../numericexpressioninput/NumericExpressionInputModuleTest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModuleTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModuleTest.kt index b12cc16af95..9a5b411670d 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModuleTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModuleTest.kt @@ -31,8 +31,7 @@ import javax.inject.Singleton @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class NumericExpressionInputModuleTest { - @Inject - @NumericExpressionInputRules + @field:[Inject NumericExpressionInputRules] lateinit var numericExpressionInputClassifiers: Map @Before From 812b66e1b0aa34f6109b7ec72c8e1f37c4f4b839 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 23:13:05 -0800 Subject: [PATCH 112/134] Fix test for Gradle. --- .../AlgebraicExpressionInputModuleTest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModuleTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModuleTest.kt index 9d71d9b2a91..81560a49781 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModuleTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModuleTest.kt @@ -31,8 +31,7 @@ import javax.inject.Singleton @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class AlgebraicExpressionInputModuleTest { - @Inject - @AlgebraicExpressionInputRules + @field:[Inject AlgebraicExpressionInputRules] lateinit var algebraicExpressionInputClassifiers: Map< String, @JvmSuppressWildcards RuleClassifier> From d8116366a0a84e6b2550440a0418ed77207a2caa Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Sat, 5 Feb 2022 00:35:07 -0800 Subject: [PATCH 113/134] Add tests for math equations. And, post-merge fixes. --- ...putIsEquivalentToRuleClassifierProvider.kt | 22 +- ...atchesExactlyWithRuleClassifierProvider.kt | 6 +- ...vialManipulationsRuleClassifierProvider.kt | 12 +- .../rules/mathequationinput/BUILD.bazel | 105 +++++ ...sEquivalentToRuleClassifierProviderTest.kt | 412 ++++++++++++++++++ ...esExactlyWithRuleClassifierProviderTest.kt | 397 +++++++++++++++++ ...ManipulationsRuleClassifierProviderTest.kt | 412 ++++++++++++++++++ .../MathEquationInputModuleTest.kt | 106 +++++ .../junit/OppiaParameterizedTestRunner.kt | 4 +- 9 files changed, 1462 insertions(+), 14 deletions(-) create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/BUILD.bazel create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProviderTest.kt create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProviderTest.kt create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModuleTest.kt diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt index 62552bb92b9..de26cf01fea 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt @@ -11,7 +11,10 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingRes import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation import org.oppia.android.util.math.toPolynomial import javax.inject.Inject -import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo +import org.oppia.android.util.math.minus +import org.oppia.android.util.math.sort +import org.oppia.android.util.math.unaryMinus class MathEquationInputIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -34,9 +37,20 @@ class MathEquationInputIsEquivalentToRuleClassifierProvider @Inject constructor( val (answerLhs, answerRhs) = parsePolynomials(answer, allowedVariables) ?: return false val (inputLhs, inputRhs) = parsePolynomials(input, allowedVariables) ?: return false - // Sides may cross-match (i.e. it's fine to reorder around the '='). - return (answerLhs.approximatelyEquals(inputLhs) && answerRhs.approximatelyEquals(inputRhs)) || - (answerLhs.approximatelyEquals(inputRhs) && answerRhs.approximatelyEquals(inputLhs)) + val newAnswerLhs = (answerLhs - answerRhs).sort() + val newInputLhs = (inputLhs - inputRhs).sort() + val negativeAnswerLhs = (-newAnswerLhs).sort() + val negativeInputLhs = (-newInputLhs).sort() + + // By subtracting the right-hand sides of both equations with their left-hand sides, the + // right-hand side becomes zero for both and implicitly equal. If the new simplified left-hand + // sides are equal then the equations are equivalent regardless of how they were originally + // arranged. Furthermore, the '-1' check is correct since the order of the equation can flip + // depending on how it was inputted, and '-1 * 0=0' so the new right-hand side remains + // unaffected. + return newAnswerLhs.isApproximatelyEqualTo(newInputLhs) + || negativeAnswerLhs.isApproximatelyEqualTo(newInputLhs) + || newAnswerLhs.isApproximatelyEqualTo(negativeInputLhs) } private fun parsePolynomials( diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt index 8c9e79a9e9f..8cbf7b08b96 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt @@ -10,7 +10,7 @@ import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation import javax.inject.Inject -import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo class MathEquationInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -62,8 +62,8 @@ class MathEquationInputMatchesExactlyWithRuleClassifierProvider @Inject construc } private fun MathEquation.approximatelyEquals(other: MathEquation): Boolean { - return leftSide.approximatelyEquals(other.leftSide) - && rightSide.approximatelyEquals(other.rightSide) + return leftSide.isApproximatelyEqualTo(other.leftSide) + && rightSide.isApproximatelyEqualTo(other.rightSide) } } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index 690e5f3646b..207161b7c8d 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -1,6 +1,6 @@ package org.oppia.android.domain.classify.rules.mathequationinput -import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.ComparableOperation import org.oppia.android.app.model.InteractionObject import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier @@ -9,9 +9,9 @@ import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation -import org.oppia.android.util.math.toComparableOperationList +import org.oppia.android.util.math.toComparableOperation import javax.inject.Inject -import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo class MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider @Inject constructor( @@ -36,18 +36,18 @@ class MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider val (inputLhs, inputRhs) = parseComparableLists(input, allowedVariables) ?: return false // Sides must match (reordering around the '=' is not allowed by this classifier). - return answerLhs.approximatelyEquals(inputLhs) && answerRhs.approximatelyEquals(inputRhs) + return answerLhs.isApproximatelyEqualTo(inputLhs) && answerRhs.isApproximatelyEqualTo(inputRhs) } private fun parseComparableLists( rawEquation: String, allowedVariables: List - ): Pair? { + ): Pair? { return when (val eqResult = parseAlgebraicEquation(rawEquation, allowedVariables)) { is MathParsingResult.Success -> { val lhsExp = eqResult.result.leftSide val rhsExp = eqResult.result.rightSide - lhsExp.toComparableOperationList() to rhsExp.toComparableOperationList() + lhsExp.toComparableOperation() to rhsExp.toComparableOperation() } is MathParsingResult.Failure -> { consoleLogger.e( diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/BUILD.bazel new file mode 100644 index 00000000000..9cfc129f250 --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/BUILD.bazel @@ -0,0 +1,105 @@ +""" +Tests for math equation input classifiers. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("//:oppia_android_test.bzl", "oppia_android_test") + +oppia_android_test( + name = "MathEquationInputIsEquivalentToRuleClassifierProviderTest", + srcs = ["MathEquationInputIsEquivalentToRuleClassifierProviderTest.kt"], + custom_package = "org.oppia.android.domain.classify.rules.mathequationinput", + test_class = "org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputIsEquivalentToRuleClassifierProviderTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_core", + "//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/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + +oppia_android_test( + name = "MathEquationInputMatchesExactlyWithRuleClassifierProviderTest", + srcs = ["MathEquationInputMatchesExactlyWithRuleClassifierProviderTest.kt"], + custom_package = "org.oppia.android.domain.classify.rules.mathequationinput", + test_class = "org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputMatchesExactlyWithRuleClassifierProviderTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_core", + "//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/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + +oppia_android_test( + name = "MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest", + srcs = ["MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt"], + custom_package = "org.oppia.android.domain.classify.rules.mathequationinput", + test_class = "org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_core", + "//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/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + +oppia_android_test( + name = "MathEquationInputModuleTest", + srcs = ["MathEquationInputModuleTest.kt"], + custom_package = "org.oppia.android.domain.classify.rules.mathequationinput", + test_class = "org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModuleTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//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/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + +dagger_rules() diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProviderTest.kt new file mode 100644 index 00000000000..11d8bbbaac1 --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProviderTest.kt @@ -0,0 +1,412 @@ +package org.oppia.android.domain.classify.rules.mathequationinput + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.SchemaObject +import org.oppia.android.app.model.SchemaObjectList +import org.oppia.android.domain.classify.ClassificationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner +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.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Tests for [MathEquationInputIsEquivalentToRuleClassifierProvider]. + * + * Note that the tests implemented in this suite are specifically set up to verify the cases + * outlined in this sheet: + * https://docs.google.com/spreadsheets/d/1u1fQdah2WsmdYKWKGmuXy5TPT7Ot-b8A7O9iZF-j5XE/edit#gid=0. + */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class MathEquationInputIsEquivalentToRuleClassifierProviderTest { + @Inject + internal lateinit var provider: MathEquationInputIsEquivalentToRuleClassifierProvider + + @Parameter lateinit var answer: String + @Parameter lateinit var input: String + + private lateinit var classifier: RuleClassifier + private val allPossibleVariables = (('a'..'z') + ('A'..'Z')).toList().map { it.toString() } + + @Before + fun setUp() { + setUpTestApplicationComponent() + classifier = provider.createRuleClassifier() + } + + @Test + @RunParameterized( + Iteration("y=1!=y=1", "answer=y=1", "input=y=1"), + Iteration("1=y!=1=y", "answer=1=y", "input=1=y") + ) + fun testMatches_answerHasDisallowedVariable_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression, allowedVariables = listOf()) + + // Despite the answer otherwise being equal, the variable isn't allowed. This shouldn't actually + // be the case in practice since neither the creator nor the learner would be allowed to input a + // disallowed variable (so this check is mainly a "just-in-case"). + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("0=1==0=1", "answer=0=1", "input=0=1"), + Iteration("y=0==y=0", "answer=y=0", "input=y=0"), + Iteration("y=1==y=1", "answer=y=1", "input=y=1"), + Iteration("0=y==0=y", "answer=0=y", "input=0=y"), + Iteration("1=y==1=y", "answer=1=y", "input=1=y"), + Iteration("y=x==y=x", "answer=y=x", "input=y=x"), + Iteration("x=y==x=y", "answer=x=y", "input=x=y") + ) + fun testMatches_sameSingleTerms_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y=-x==y=-x", "answer=y=-x", "input=y=-x"), + Iteration("-y=x==-y=x", "answer=-y=x", "input=-y=x"), + Iteration("y=3.14+x==y=3.14+x", "answer=y=3.14+x", "input=y=3.14+x"), + Iteration("y=x+y+z==y=x+y+z", "answer=y=x+y+z", "input=y=x+y+z"), + Iteration("y=x/2/3==y=x/2/3", "answer=y=x/2/3", "input=y=x/2/3"), + Iteration("y=x^2==y=x^2", "answer=y=x^2", "input=y=x^2") + ) + fun testMatches_sameSingleOperations_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y=x/y/z!=y=x/y/z", "answer=y=x/y/z", "input=y=x/y/z"), + Iteration("y=sqrt(x)!=y=sqrt(x)", "answer=y=sqrt(x)", "input=y=sqrt(x)") + ) + fun testMatches_sameSingleOperations_thatCannotBecomePolynomials_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Despite the terms being the same, if they can't be converted to a polynomial then the + // classifier won't match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("y=1!=y=0", "answer=y=1", "input=y=0"), + Iteration("y=0!=y=1", "answer=y=0", "input=y=1"), + Iteration("y=3.14!=y=1", "answer=y=3.14", "input=y=1"), + Iteration("y=1!=y=3.14", "answer=y=1", "input=y=3.14"), + Iteration("y=x!=y=3.14", "answer=y=x", "input=y=3.14"), + Iteration("y=1!=y=x", "answer=y=1", "input=y=x"), + Iteration("y=3.14!=y=x", "answer=y=3.14", "input=y=x"), + Iteration("y=z!=y=x", "answer=y=z", "input=y=x"), + Iteration("y=x!=y=z", "answer=y=x", "input=y=z") + ) + fun testMatches_differentSingleTerms_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions don't evaluate to the same value then the classifier won't match them. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("y=1+x==y=x+1", "answer=y=1+x", "input=y=x+1"), + Iteration("y=z+x==y=x+z", "answer=y=z+x", "input=y=x+z"), + Iteration("y+1=x==1+y=x", "answer=y+1=x", "input=1+y=x"), + Iteration("y+z=x==z+y=x", "answer=y+z=x", "input=z+y=x"), + Iteration("x+y=1+z==y+x=z+1", "answer=x+y=1+z", "input=y+x=z+1"), + Iteration("y=-x+1==y=1-x", "answer=y=-x+1", "input=y=1-x"), + Iteration("-y+x=z==x-y=z", "answer=-y+x=z", "input=x-y=z"), + Iteration("y=x*2==y=2x", "answer=y=x*2", "input=y=2x"), + Iteration("y*2=z==2y=z", "answer=y*2=z", "input=2y=z"), + Iteration("y=3*2==y=2*3", "answer=y=3*2", "input=y=2*3"), + Iteration("y=zx==y=xz", "answer=y=zx", "input=y=xz"), + Iteration("yx=z==xy=z", "answer=yx=z", "input=xy=z") + ) + fun testMatches_operationsDiffer_byCommutativity_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Reordering terms by commutativity is allowed by this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y=1+(2+3)==y=(1+2)+3", "answer=y=1+(2+3)", "input=y=(1+2)+3"), + Iteration("y=x+(y+z)==y=(x+y)+z", "answer=y=x+(y+z)", "input=y=(x+y)+z"), + Iteration("x+(y+z)=1==(x+y)+z=1", "answer=x+(y+z)=1", "input=(x+y)+z=1"), + Iteration( + "(x+y)+z=1+(2+3)==x+(y+z)=(1+2)+3", "answer=(x+y)+z=1+(2+3)", "input=x+(y+z)=(1+2)+3" + ), + Iteration("y=2*(3*4)==y=(2*3)*4", "answer=y=2*(3*4)", "input=y=(2*3)*4"), + Iteration("y=2*(3x)==y=(2x)*3", "answer=y=2*(3x)", "input=y=(2x)*3"), + Iteration("y=x(yz)==y=(xy)z", "answer=y=x(yz)", "input=y=(xy)z"), + Iteration("x(yz)=2==(xy)z=2", "answer=x(yz)=2", "input=(xy)z=2"), + Iteration("2*(3y)=4==(2y)*3=4", "answer=2*(3y)=4", "input=(2y)*3=4"), + Iteration("x(yz)=(2*3)*4==(xy)z=2*(3*4)", "answer=x(yz)=(2*3)*4", "input=(xy)z=2*(3*4)") + ) + fun testMatches_operationsDiffer_byAssociativity_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Changing operation associativity is allowed by this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y=3.14-1!=y=1-3.14", "answer=y=3.14-1", "input=y=1-3.14"), + Iteration("y=1-(2-3)!=y=(1-2)-3", "answer=y=1-(2-3)", "input=y=(1-2)-3"), + Iteration("y-1=3!=1-y=3", "answer=y-1=3", "input=1-y=3"), + Iteration("x-(y-z)=3!=(x-y)-z=3", "answer=x-(y-z)=3", "input=(x-y)-z=3"), + Iteration("y=3.14/x!=y=x/3.14", "answer=y=3.14/x", "input=y=x/3.14"), + Iteration("y/x=2!=x/y=2", "answer=y/x=2", "input=x/y=2"), + Iteration("y=3.14^2!=y=2^3.14", "answer=y=3.14^2", "input=y=2^3.14"), + Iteration("(3.14^2)y=2!=(2^3.14)y=2", "answer=(3.14^2)y=2", "input=(2^3.14)y=2"), + Iteration("y=x/(y/z)!=y=(x/y)/z", "answer=y=x/(y/z)", "input=y=(x/y)/z"), + Iteration("x/(y/z)=2!=(x/y)/z=2", "answer=x/(y/z)=2", "input=(x/y)/z=2") + ) + fun testMatches_operationsDiffer_byNonCommutativeOrAssociativeReordering_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Non-commutative and non-associative reordering generally results in a different value, so the + // classifier will fail to match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("y=1-2==y=-(2-1)", "answer=y=1-2", "input=y=-(2-1)"), + Iteration("y=1+2==y=1+1+1", "answer=y=1+2", "input=y=1+1+1"), + Iteration("y=1+2==y=1-(-2)", "answer=y=1+2", "input=y=1-(-2)"), + Iteration("y=4-6==y=1-2-1", "answer=y=4-6", "input=y=1-2-1"), + Iteration("y=2*3*2*2==y=2*3*4", "answer=y=2*3*2*2", "input=y=2*3*4"), + Iteration("y=-6-2==y=2*-(3+1)", "answer=y=-6-2", "input=y=2*-(3+1)"), + Iteration("y=2/3/2/2==y=2/3/4", "answer=y=2/3/2/2", "input=y=2/3/4"), + Iteration("y=2^(2+1)==y=2^3", "answer=y=2^(2+1)", "input=y=2^3"), + Iteration("y=2^(-1)==y=1/2", "answer=y=2^(-1)", "input=y=1/2"), + Iteration("z=x-y==z=-(y-x)", "answer=z=x-y", "input=z=-(y-x)"), + Iteration("y=2+x==y=1+x+1", "answer=y=2+x", "input=y=1+x+1"), + Iteration("y=1+x==y=1-(-x)", "answer=y=1+x", "input=y=1-(-x)"), + Iteration("y=-x==y=1-x-1", "answer=y=-x", "input=y=1-x-1"), + Iteration("y=4x==y=2*2*x", "answer=y=4x", "input=y=2*2*x"), + Iteration("y=2-6x==y=2*(-3x+1)", "answer=y=2-6x", "input=y=2*(-3x+1)"), + Iteration("y=x/4==y=x/2/2", "answer=y=x/4", "input=y=x/2/2"), + Iteration("y=x^(2+1)==y=x^3", "answer=y=x^(2+1)", "input=y=x^3"), + Iteration("y=x*(2^(-1))==y=x/2", "answer=y=x*(2^(-1))", "input=y=x/2"), + Iteration("y+2=x==1+1+y=x", "answer=y+2=x", "input=1+1+y=x"), + Iteration("(2^2)y=x+2==4y=x+2", "answer=(2^2)y=x+2", "input=4y=x+2"), + Iteration("y^(4-2)=3x==y^2=3x", "answer=y^(4-2)=3x", "input=y^2=3x"), + Iteration("y/2/2=3x==y/4=3x", "answer=y/2/2=3x", "input=y/4=3x") + ) + fun testMatches_operationsDiffer_byDistributionAndCombining_returnTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier supports any distribution or combining of terms. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("2+x=y==y=2+x", "answer=2+x=y", "input=y=2+x"), + Iteration("-3x^3=2y^2==2y^2=-3x^3", "answer=-3x^3=2y^2", "input=2y^2=-3x^3"), + Iteration("-4+x=2+y+1-1==2+y=x-4", "answer=-4+x=2+y+1-1", "input=2+y=x-4"), + Iteration("y=x-6==2+y=x-4", "answer=y=x-6", "input=2+y=x-4"), + Iteration("(1+1+1)*x=2*y/4==y/2=3x", "answer=(1+1+1)*x=2*y/4", "input=y/2=3x") + ) + fun testMatches_sidesRearrangedAroundEqualsSign_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // The classifier should match regardless of how the equation is laid out so long as it's equal + // without any multiples. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y = 3x^2 - 4==y = 3x^2 - 4", "answer=y = 3x^2 - 4", "input=y = 3x^2 - 4"), + Iteration("y = -4 + 3x^2==y = 3x^2 - 4", "answer=y = -4 + 3x^2", "input=y = 3x^2 - 4"), + Iteration("y = x^2*3 - 4==y = 3x^2 - 4", "answer=y = x^2*3 - 4", "input=y = 3x^2 - 4"), + Iteration("y+4=3x^2==y = 3x^2 - 4", "answer=y+4=3x^2", "input=y = 3x^2 - 4"), + Iteration("y-3x^2=-4==y = 3x^2 - 4", "answer=y-3x^2=-4", "input=y = 3x^2 - 4"), + Iteration("-4=y-3x^2==y = 3x^2 - 4", "answer=-4=y-3x^2", "input=y = 3x^2 - 4"), + Iteration("3x^2-y=4==y = 3x^2 - 4", "answer=3x^2-y=4", "input=y = 3x^2 - 4"), + Iteration("3x^2=4+y==y = 3x^2 - 4", "answer=3x^2=4+y", "input=y = 3x^2 - 4"), + Iteration("y-x^2=2x^2-4==y = 3x^2 - 4", "answer=y-x^2=2x^2-4", "input=y = 3x^2 - 4"), + Iteration("y=x*(2^(1/2))==y=sqrt(2)x", "answer=y=x*(2^(1/2))", "input=y=sqrt(2)x"), + Iteration("y − 3x^2 = -4==y = 3x^2 - 4", "answer=y − 3x^2 = -4", "input=y = 3x^2 - 4"), + Iteration("3x^2 - 4 = y==y = 3x^2 - 4", "answer=3x^2 - 4 = y", "input=y = 3x^2 - 4"), + Iteration("y − 3x^2 + 4 = 0==y = 3x^2 - 4", "answer=y − 3x^2 + 4 = 0", "input=y = 3x^2 - 4"), + Iteration("y = (3x^3 - 4x)/x==y = 3x^2 - 4", "answer=y = (3x^3 - 4x)/x", "input=y = 3x^2 - 4"), + Iteration("y^2/y = 3x^2 - 4==y = 3x^2 - 4", "answer=y^2/y = 3x^2 - 4", "input=y = 3x^2 - 4") + ) + fun testMatches_assortedExpressions_withMatchingCharacteristics_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // pass for this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y = 3x^2 - 7!=y = 3x^2 - 4", "answer=y = 3x^2 - 7", "input=y = 3x^2 - 4"), + Iteration("x^2 = 3y - 4!=y = 3x^2 - 4", "answer=x^2 = 3y - 4", "input=y = 3x^2 - 4"), + Iteration("y/(3x^2 - 4) = 1!=y = 3x^2 - 4", "answer=y/(3x^2 - 4) = 1", "input=y = 3x^2 - 4"), + Iteration("y + 3x^2 - 4 = 0!=y = 3x^2 - 4", "answer=y + 3x^2 - 4 = 0", "input=y = 3x^2 - 4"), + Iteration("y^2 = 3x^2y - 4y!=y = 3x^2 - 4", "answer=y^2 = 3x^2y - 4y", "input=y = 3x^2 - 4"), + Iteration( + "y^2 * y^−1 = -12x^2!=y = 3x^2 - 4", "answer=y^2 * y^−1 = -12x^2", "input=y = 3x^2 - 4" + ), + Iteration("2 − 3 = -4!=y = 3x^2 - 4", "answer=2 − 3 = -4", "input=y = 3x^2 - 4"), + Iteration("y = 3x^2 + 4!=y = 3x^2 - 4", "answer=y = 3x^2 + 4", "input=y = 3x^2 - 4"), + Iteration("y - 4 = 3x^2!=y = 3x^2 - 4", "answer=y - 4 = 3x^2", "input=y = 3x^2 - 4"), + Iteration("y − 3x^2 - 4 = 0!=y = 3x^2 - 4", "answer=y − 3x^2 - 4 = 0", "input=y = 3x^2 - 4"), + Iteration("0 = y + 3x^2 - 4!=y = 3x^2 - 4", "answer=0 = y + 3x^2 - 4", "input=y = 3x^2 - 4"), + Iteration("2 = 0!=y = 3x^2 - 4", "answer=2 = 0", "input=y = 3x^2 - 4"), + Iteration("y=3x-4!=y = 3x^2 - 4", "answer=y=3x-4", "input=y = 3x^2 - 4"), + Iteration("y - x^2 = -4!=y = 3x^2 - 4", "answer=y - x^2 = -4", "input=y = 3x^2 - 4"), + Iteration("y/sqrt(2)=x!=y=sqrt(2)x", "answer=y/sqrt(2)=x", "input=y=sqrt(2)x"), + Iteration("y/4=x!=y=4x", "answer=y/4=x", "input=y=4x"), + Iteration("y/4=16x!=y=4x", "answer=y/4=16x", "input=y=4x"), + Iteration("xy=x^2!=y=x", "answer=xy=x^2", "input=y=x") + ) + fun testMatches_assortedExpressions_withoutMatchingCharacteristics_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // not pass for this classifier. + assertThat(matches).isFalse() + } + + private fun matchesClassifier( + answerExpression: InteractionObject, + inputExpression: InteractionObject, + allowedVariables: List = allPossibleVariables + ): Boolean { + return classifier.matches( + answerExpression, + inputs = mapOf("x" to inputExpression), + classificationContext = ClassificationContext( + customizationArgs = mapOf( + "customOskLetters" to SchemaObject.newBuilder().apply { + schemaObjectList = SchemaObjectList.newBuilder().apply { + addAllSchemaObject( + allowedVariables.map { + SchemaObject.newBuilder().setNormalizedString(it).build() + } + ) + }.build() + }.build() + ) + ) + ) + } + + private fun createMathExpression(rawExpression: String) = InteractionObject.newBuilder().apply { + mathExpression = rawExpression + }.build() + + private fun setUpTestApplicationComponent() { + DaggerMathEquationInputIsEquivalentToRuleClassifierProviderTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestDispatcherModule::class, LoggerModule::class, RobolectricModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(test: MathEquationInputIsEquivalentToRuleClassifierProviderTest) + } +} diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProviderTest.kt new file mode 100644 index 00000000000..7c8a7d596d1 --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProviderTest.kt @@ -0,0 +1,397 @@ +package org.oppia.android.domain.classify.rules.mathequationinput + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.SchemaObject +import org.oppia.android.app.model.SchemaObjectList +import org.oppia.android.domain.classify.ClassificationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner +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.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Tests for [MathEquationInputMatchesExactlyWithRuleClassifierProvider]. + * + * Note that the tests implemented in this suite are specifically set up to verify the cases + * outlined in this sheet: + * https://docs.google.com/spreadsheets/d/1u1fQdah2WsmdYKWKGmuXy5TPT7Ot-b8A7O9iZF-j5XE/edit#gid=0. + */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class MathEquationInputMatchesExactlyWithRuleClassifierProviderTest { + @Inject + internal lateinit var provider: MathEquationInputMatchesExactlyWithRuleClassifierProvider + + @Parameter lateinit var answer: String + @Parameter lateinit var input: String + + private lateinit var classifier: RuleClassifier + private val allPossibleVariables = (('a'..'z') + ('A'..'Z')).toList().map { it.toString() } + + @Before + fun setUp() { + setUpTestApplicationComponent() + classifier = provider.createRuleClassifier() + } + + @Test + @RunParameterized( + Iteration("y=1!=y=1", "answer=y=1", "input=y=1"), + Iteration("1=y!=1=y", "answer=1=y", "input=1=y") + ) + fun testMatches_answerHasDisallowedVariable_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression, allowedVariables = listOf()) + + // Despite the answer otherwise being equal, the variable isn't allowed. This shouldn't actually + // be the case in practice since neither the creator nor the learner would be allowed to input a + // disallowed variable (so this check is mainly a "just-in-case"). + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("0=1==0=1", "answer=0=1", "input=0=1"), + Iteration("y=0==y=0", "answer=y=0", "input=y=0"), + Iteration("y=1==y=1", "answer=y=1", "input=y=1"), + Iteration("0=y==0=y", "answer=0=y", "input=0=y"), + Iteration("1=y==1=y", "answer=1=y", "input=1=y"), + Iteration("y=x==y=x", "answer=y=x", "input=y=x"), + Iteration("x=y==x=y", "answer=x=y", "input=x=y") + ) + fun testMatches_sameSingleTerms_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y=-x==y=-x", "answer=y=-x", "input=y=-x"), + Iteration("-y=x==-y=x", "answer=-y=x", "input=-y=x"), + Iteration("y=3.14+x==y=3.14+x", "answer=y=3.14+x", "input=y=3.14+x"), + Iteration("y=x+y+z==y=x+y+z", "answer=y=x+y+z", "input=y=x+y+z"), + Iteration("y=x/y/z==y=x/y/z", "answer=y=x/y/z", "input=y=x/y/z"), + Iteration("y=x/2/3==y=x/2/3", "answer=y=x/2/3", "input=y=x/2/3"), + Iteration("y=x^2==y=x^2", "answer=y=x^2", "input=y=x^2"), + Iteration("y=sqrt(x)==y=sqrt(x)", "answer=y=sqrt(x)", "input=y=sqrt(x)") + ) + fun testMatches_sameSingleOperations_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y=1!=y=0", "answer=y=1", "input=y=0"), + Iteration("y=0!=y=1", "answer=y=0", "input=y=1"), + Iteration("y=3.14!=y=1", "answer=y=3.14", "input=y=1"), + Iteration("y=1!=y=3.14", "answer=y=1", "input=y=3.14"), + Iteration("y=x!=y=3.14", "answer=y=x", "input=y=3.14"), + Iteration("y=1!=y=x", "answer=y=1", "input=y=x"), + Iteration("y=3.14!=y=x", "answer=y=3.14", "input=y=x"), + Iteration("y=z!=y=x", "answer=y=z", "input=y=x"), + Iteration("y=x!=y=z", "answer=y=x", "input=y=z") + ) + fun testMatches_differentSingleTerms_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions aren't exactly the same (minus whitespace), they won't match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("y=1+x!=y=x+1", "answer=y=1+x", "input=y=x+1"), + Iteration("y=z+x!=y=x+z", "answer=y=z+x", "input=y=x+z"), + Iteration("y+1=x!=1+y=x", "answer=y+1=x", "input=1+y=x"), + Iteration("y+z=x!=z+y=x", "answer=y+z=x", "input=z+y=x"), + Iteration("x+y=1+z!=y+x=z+1", "answer=x+y=1+z", "input=y+x=z+1"), + Iteration("y=-x+1!=y=1-x", "answer=y=-x+1", "input=y=1-x"), + Iteration("-y+x=z!=x-y=z", "answer=-y+x=z", "input=x-y=z"), + Iteration("y=x*2!=y=2x", "answer=y=x*2", "input=y=2x"), + Iteration("y*2=z!=2y=z", "answer=y*2=z", "input=2y=z"), + Iteration("y=3*2!=y=2*3", "answer=y=3*2", "input=y=2*3"), + Iteration("y=zx!=y=xz", "answer=y=zx", "input=y=xz"), + Iteration("yx=z!=xy=z", "answer=yx=z", "input=xy=z") + ) + fun testMatches_operationsDiffer_byCommutativity_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier expects commutativity to be retained. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("y=1+(2+3)!=y=(1+2)+3", "answer=y=1+(2+3)", "input=y=(1+2)+3"), + Iteration("y=x+(y+z)!=y=(x+y)+z", "answer=y=x+(y+z)", "input=y=(x+y)+z"), + Iteration("x+(y+z)=1!=(x+y)+z=1", "answer=x+(y+z)=1", "input=(x+y)+z=1"), + Iteration( + "(x+y)+z=1+(2+3)!=x+(y+z)=(1+2)+3", "answer=(x+y)+z=1+(2+3)", "input=x+(y+z)=(1+2)+3" + ), + Iteration("y=2*(3*4)!=y=(2*3)*4", "answer=y=2*(3*4)", "input=y=(2*3)*4"), + Iteration("y=2*(3x)!=y=(2x)*3", "answer=y=2*(3x)", "input=y=(2x)*3"), + Iteration("y=x(yz)!=y=(xy)z", "answer=y=x(yz)", "input=y=(xy)z"), + Iteration("x(yz)=2!=(xy)z=2", "answer=x(yz)=2", "input=(xy)z=2"), + Iteration("2*(3y)=4!=(2y)*3=4", "answer=2*(3y)=4", "input=(2y)*3=4"), + Iteration("x(yz)=(2*3)*4!=(xy)z=2*(3*4)", "answer=x(yz)=(2*3)*4", "input=(xy)z=2*(3*4)") + ) + fun testMatches_operationsDiffer_byAssociativity_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier expects associativity to be retained. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("y=3.14-1!=y=1-3.14", "answer=y=3.14-1", "input=y=1-3.14"), + Iteration("y=1-(2-3)!=y=(1-2)-3", "answer=y=1-(2-3)", "input=y=(1-2)-3"), + Iteration("y-1=3!=1-y=3", "answer=y-1=3", "input=1-y=3"), + Iteration("x-(y-z)=3!=(x-y)-z=3", "answer=x-(y-z)=3", "input=(x-y)-z=3"), + Iteration("y=3.14/x!=y=x/3.14", "answer=y=3.14/x", "input=y=x/3.14"), + Iteration("y/x=2!=x/y=2", "answer=y/x=2", "input=x/y=2"), + Iteration("y=3.14^2!=y=2^3.14", "answer=y=3.14^2", "input=y=2^3.14"), + Iteration("(3.14^2)y=2!=(2^3.14)y=2", "answer=(3.14^2)y=2", "input=(2^3.14)y=2"), + Iteration("y=x/(y/z)!=y=(x/y)/z", "answer=y=x/(y/z)", "input=y=(x/y)/z"), + Iteration("x/(y/z)=2!=(x/y)/z=2", "answer=x/(y/z)=2", "input=(x/y)/z=2") + ) + fun testMatches_operationsDiffer_byNonCommutativeOrAssociativeReordering_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Non-commutative and non-associative reordering generally results in a different value, so the + // classifier will fail to match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("y=1-2!=y=-(2-1)", "answer=y=1-2", "input=y=-(2-1)"), + Iteration("y=1+2!=y=1+1+1", "answer=y=1+2", "input=y=1+1+1"), + Iteration("y=1+2!=y=1-(-2)", "answer=y=1+2", "input=y=1-(-2)"), + Iteration("y=4-6!=y=1-2-1", "answer=y=4-6", "input=y=1-2-1"), + Iteration("y=2*3*2*2!=y=2*3*4", "answer=y=2*3*2*2", "input=y=2*3*4"), + Iteration("y=-6-2!=y=2*-(3+1)", "answer=y=-6-2", "input=y=2*-(3+1)"), + Iteration("y=2/3/2/2!=y=2/3/4", "answer=y=2/3/2/2", "input=y=2/3/4"), + Iteration("y=2^(2+1)!=y=2^3", "answer=y=2^(2+1)", "input=y=2^3"), + Iteration("y=2^(-1)!=y=1/2", "answer=y=2^(-1)", "input=y=1/2"), + Iteration("z=x-y!=z=-(y-x)", "answer=z=x-y", "input=z=-(y-x)"), + Iteration("y=2+x!=y=1+x+1", "answer=y=2+x", "input=y=1+x+1"), + Iteration("y=1+x!=y=1-(-x)", "answer=y=1+x", "input=y=1-(-x)"), + Iteration("y=-x!=y=1-x-1", "answer=y=-x", "input=y=1-x-1"), + Iteration("y=4x!=y=2*2*x", "answer=y=4x", "input=y=2*2*x"), + Iteration("y=2-6x!=y=2*(-3x+1)", "answer=y=2-6x", "input=y=2*(-3x+1)"), + Iteration("y=x/4!=y=x/2/2", "answer=y=x/4", "input=y=x/2/2"), + Iteration("y=x^(2+1)!=y=x^3", "answer=y=x^(2+1)", "input=y=x^3"), + Iteration("y=x*(2^(-1))!=y=x/2", "answer=y=x*(2^(-1))", "input=y=x/2"), + Iteration("y+2=x!=1+1+y=x", "answer=y+2=x", "input=1+1+y=x"), + Iteration("(2^2)y=x+2!=4y=x+2", "answer=(2^2)y=x+2", "input=4y=x+2"), + Iteration("y^(4-2)=3x!=y^2=3x", "answer=y^(4-2)=3x", "input=y^2=3x"), + Iteration("y/2/2=3x!=y/4=3x", "answer=y/2/2=3x", "input=y/4=3x") + ) + fun testMatches_operationsDiffer_byDistributionAndCombining_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier doesn't support distributing or combining terms. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("2+x=y!=y=2+x", "answer=2+x=y", "input=y=2+x"), + Iteration("-3x^3=2y^2!=2y^2=-3x^3", "answer=-3x^3=2y^2", "input=2y^2=-3x^3"), + Iteration("-4+x=2+y+1-1!=2+y=x-4", "answer=-4+x=2+y+1-1", "input=2+y=x-4"), + Iteration("y=x-6!=2+y=x-4", "answer=y=x-6", "input=2+y=x-4"), + Iteration("(1+1+1)*x=2*y/4!=y/2=3x", "answer=(1+1+1)*x=2*y/4", "input=y/2=3x") + ) + fun testMatches_sidesRearrangedAroundEqualsSign_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier doesn't support rearranging the left or right-hand sides of the equation. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("y = 3x^2 - 4==y = 3x^2 - 4", "answer=y = 3x^2 - 4", "input=y = 3x^2 - 4") + ) + fun testMatches_assortedExpressions_withMatchingCharacteristics_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // pass for this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y = -4 + 3x^2!=y = 3x^2 - 4", "answer=y = -4 + 3x^2", "input=y = 3x^2 - 4"), + Iteration("y = x^2*3 - 4!=y = 3x^2 - 4", "answer=y = x^2*3 - 4", "input=y = 3x^2 - 4"), + Iteration("y+4=3x^2!=y = 3x^2 - 4", "answer=y+4=3x^2", "input=y = 3x^2 - 4"), + Iteration("y-3x^2=-4!=y = 3x^2 - 4", "answer=y-3x^2=-4", "input=y = 3x^2 - 4"), + Iteration("-4=y-3x^2!=y = 3x^2 - 4", "answer=-4=y-3x^2", "input=y = 3x^2 - 4"), + Iteration("3x^2-y=4!=y = 3x^2 - 4", "answer=3x^2-y=4", "input=y = 3x^2 - 4"), + Iteration("3x^2=4+y!=y = 3x^2 - 4", "answer=3x^2=4+y", "input=y = 3x^2 - 4"), + Iteration("y-x^2=2x^2-4!=y = 3x^2 - 4", "answer=y-x^2=2x^2-4", "input=y = 3x^2 - 4"), + Iteration("y=x*(2^(1/2))!=y=sqrt(2)x", "answer=y=x*(2^(1/2))", "input=y=sqrt(2)x"), + Iteration("y − 3x^2 = -4!=y = 3x^2 - 4", "answer=y − 3x^2 = -4", "input=y = 3x^2 - 4"), + Iteration("3x^2 - 4 = y!=y = 3x^2 - 4", "answer=3x^2 - 4 = y", "input=y = 3x^2 - 4"), + Iteration("y = 3x^2 - 7!=y = 3x^2 - 4", "answer=y = 3x^2 - 7", "input=y = 3x^2 - 4"), + Iteration("x^2 = 3y - 4!=y = 3x^2 - 4", "answer=x^2 = 3y - 4", "input=y = 3x^2 - 4"), + Iteration("y/(3x^2 - 4) = 1!=y = 3x^2 - 4", "answer=y/(3x^2 - 4) = 1", "input=y = 3x^2 - 4"), + Iteration("y + 3x^2 - 4 = 0!=y = 3x^2 - 4", "answer=y + 3x^2 - 4 = 0", "input=y = 3x^2 - 4"), + Iteration("y − 3x^2 + 4 = 0!=y = 3x^2 - 4", "answer=y − 3x^2 + 4 = 0", "input=y = 3x^2 - 4"), + Iteration("y^2 = 3x^2y - 4y!=y = 3x^2 - 4", "answer=y^2 = 3x^2y - 4y", "input=y = 3x^2 - 4"), + Iteration("y = (3x^3 - 4x)/x!=y = 3x^2 - 4", "answer=y = (3x^3 - 4x)/x", "input=y = 3x^2 - 4"), + Iteration( + "y^2 * y^−1 = -12x^2!=y = 3x^2 - 4", "answer=y^2 * y^−1 = -12x^2", "input=y = 3x^2 - 4" + ), + Iteration("y^2/y = 3x^2 - 4!=y = 3x^2 - 4", "answer=y^2/y = 3x^2 - 4", "input=y = 3x^2 - 4"), + Iteration("2 − 3 = -4!=y = 3x^2 - 4", "answer=2 − 3 = -4", "input=y = 3x^2 - 4"), + Iteration("y = 3x^2 + 4!=y = 3x^2 - 4", "answer=y = 3x^2 + 4", "input=y = 3x^2 - 4"), + Iteration("y - 4 = 3x^2!=y = 3x^2 - 4", "answer=y - 4 = 3x^2", "input=y = 3x^2 - 4"), + Iteration("y − 3x^2 - 4 = 0!=y = 3x^2 - 4", "answer=y − 3x^2 - 4 = 0", "input=y = 3x^2 - 4"), + Iteration("0 = y + 3x^2 - 4!=y = 3x^2 - 4", "answer=0 = y + 3x^2 - 4", "input=y = 3x^2 - 4"), + Iteration("2 = 0!=y = 3x^2 - 4", "answer=2 = 0", "input=y = 3x^2 - 4"), + Iteration("y - x^2 = -4!=y = 3x^2 - 4", "answer=y - x^2 = -4", "input=y = 3x^2 - 4"), + Iteration("y=3x-4!=y = 3x^2 - 4", "answer=y=3x-4", "input=y = 3x^2 - 4"), + Iteration("y/sqrt(2)=x!=y=sqrt(2)x", "answer=y/sqrt(2)=x", "input=y=sqrt(2)x"), + Iteration("y/4=x!=y=4x", "answer=y/4=x", "input=y=4x"), + Iteration("y/4=16x!=y=4x", "answer=y/4=16x", "input=y=4x"), + Iteration("xy=x^2!=y=x", "answer=xy=x^2", "input=y=x") + ) + fun testMatches_assortedExpressions_withoutMatchingCharacteristics_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // not pass for this classifier. + assertThat(matches).isFalse() + } + + private fun matchesClassifier( + answerExpression: InteractionObject, + inputExpression: InteractionObject, + allowedVariables: List = allPossibleVariables + ): Boolean { + return classifier.matches( + answerExpression, + inputs = mapOf("x" to inputExpression), + classificationContext = ClassificationContext( + customizationArgs = mapOf( + "customOskLetters" to SchemaObject.newBuilder().apply { + schemaObjectList = SchemaObjectList.newBuilder().apply { + addAllSchemaObject( + allowedVariables.map { + SchemaObject.newBuilder().setNormalizedString(it).build() + } + ) + }.build() + }.build() + ) + ) + ) + } + + private fun createMathExpression(rawExpression: String) = InteractionObject.newBuilder().apply { + mathExpression = rawExpression + }.build() + + private fun setUpTestApplicationComponent() { + DaggerMathEquationInputMatchesExactlyWithRuleClassifierProviderTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestDispatcherModule::class, LoggerModule::class, RobolectricModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(test: MathEquationInputMatchesExactlyWithRuleClassifierProviderTest) + } +} diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt new file mode 100644 index 00000000000..604ab230cb3 --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt @@ -0,0 +1,412 @@ +package org.oppia.android.domain.classify.rules.mathequationinput + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.SchemaObject +import org.oppia.android.app.model.SchemaObjectList +import org.oppia.android.domain.classify.ClassificationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner +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.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Tests for [MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider]. + * + * Note that the tests implemented in this suite are specifically set up to verify the cases + * outlined in this sheet: + * https://docs.google.com/spreadsheets/d/1u1fQdah2WsmdYKWKGmuXy5TPT7Ot-b8A7O9iZF-j5XE/edit#gid=0. + */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest { + @Inject internal lateinit var provider: MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider + + @Parameter lateinit var answer: String + @Parameter lateinit var input: String + + private lateinit var classifier: RuleClassifier + private val allPossibleVariables = (('a'..'z') + ('A'..'Z')).toList().map { it.toString() } + + @Before + fun setUp() { + setUpTestApplicationComponent() + classifier = provider.createRuleClassifier() + } + + @Test + @RunParameterized( + Iteration("y=1!=y=1", "answer=y=1", "input=y=1"), + Iteration("1=y!=1=y", "answer=1=y", "input=1=y") + ) + fun testMatches_answerHasDisallowedVariable_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression, allowedVariables = listOf()) + + // Despite the answer otherwise being equal, the variable isn't allowed. This shouldn't actually + // be the case in practice since neither the creator nor the learner would be allowed to input a + // disallowed variable (so this check is mainly a "just-in-case"). + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("0=1==0=1", "answer=0=1", "input=0=1"), + Iteration("y=0==y=0", "answer=y=0", "input=y=0"), + Iteration("y=1==y=1", "answer=y=1", "input=y=1"), + Iteration("0=y==0=y", "answer=0=y", "input=0=y"), + Iteration("1=y==1=y", "answer=1=y", "input=1=y"), + Iteration("y=x==y=x", "answer=y=x", "input=y=x"), + Iteration("x=y==x=y", "answer=x=y", "input=x=y") + ) + fun testMatches_sameSingleTerms_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y=-x==y=-x", "answer=y=-x", "input=y=-x"), + Iteration("-y=x==-y=x", "answer=-y=x", "input=-y=x"), + Iteration("y=3.14+x==y=3.14+x", "answer=y=3.14+x", "input=y=3.14+x"), + Iteration("y=x+y+z==y=x+y+z", "answer=y=x+y+z", "input=y=x+y+z"), + Iteration("y=x/y/z==y=x/y/z", "answer=y=x/y/z", "input=y=x/y/z"), + Iteration("y=x/2/3==y=x/2/3", "answer=y=x/2/3", "input=y=x/2/3"), + Iteration("y=x^2==y=x^2", "answer=y=x^2", "input=y=x^2"), + Iteration("y=sqrt(x)==y=sqrt(x)", "answer=y=sqrt(x)", "input=y=sqrt(x)") + ) + fun testMatches_sameSingleOperations_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y=1!=y=0", "answer=y=1", "input=y=0"), + Iteration("y=0!=y=1", "answer=y=0", "input=y=1"), + Iteration("y=3.14!=y=1", "answer=y=3.14", "input=y=1"), + Iteration("y=1!=y=3.14", "answer=y=1", "input=y=3.14"), + Iteration("y=x!=y=3.14", "answer=y=x", "input=y=3.14"), + Iteration("y=1!=y=x", "answer=y=1", "input=y=x"), + Iteration("y=3.14!=y=x", "answer=y=3.14", "input=y=x"), + Iteration("y=z!=y=x", "answer=y=z", "input=y=x"), + Iteration("y=x!=y=z", "answer=y=x", "input=y=z") + ) + fun testMatches_differentSingleTerms_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions aren't exactly the same (minus whitespace and some minor term + // reordering), they won't match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("y=1+x==y=x+1", "answer=y=1+x", "input=y=x+1"), + Iteration("y=z+x==y=x+z", "answer=y=z+x", "input=y=x+z"), + Iteration("y+1=x==1+y=x", "answer=y+1=x", "input=1+y=x"), + Iteration("y+z=x==z+y=x", "answer=y+z=x", "input=z+y=x"), + Iteration("x+y=1+z==y+x=z+1", "answer=x+y=1+z", "input=y+x=z+1"), + Iteration("y=-x+1==y=1-x", "answer=y=-x+1", "input=y=1-x"), + Iteration("-y+x=z==x-y=z", "answer=-y+x=z", "input=x-y=z"), + Iteration("y=x*2==y=2x", "answer=y=x*2", "input=y=2x"), + Iteration("y*2=z==2y=z", "answer=y*2=z", "input=2y=z"), + Iteration("y=3*2==y=2*3", "answer=y=3*2", "input=y=2*3"), + Iteration("y=zx==y=xz", "answer=y=zx", "input=y=xz"), + Iteration("yx=z==xy=z", "answer=yx=z", "input=xy=z") + ) + fun testMatches_operationsDiffer_byCommutativity_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Reordering terms by commutativity is allowed by this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y=1+(2+3)==y=(1+2)+3", "answer=y=1+(2+3)", "input=y=(1+2)+3"), + Iteration("y=x+(y+z)==y=(x+y)+z", "answer=y=x+(y+z)", "input=y=(x+y)+z"), + Iteration("x+(y+z)=1==(x+y)+z=1", "answer=x+(y+z)=1", "input=(x+y)+z=1"), + Iteration( + "(x+y)+z=1+(2+3)==x+(y+z)=(1+2)+3", "answer=(x+y)+z=1+(2+3)", "input=x+(y+z)=(1+2)+3" + ), + Iteration("y=2*(3*4)==y=(2*3)*4", "answer=y=2*(3*4)", "input=y=(2*3)*4"), + Iteration("y=2*(3x)==y=(2x)*3", "answer=y=2*(3x)", "input=y=(2x)*3"), + Iteration("y=x(yz)==y=(xy)z", "answer=y=x(yz)", "input=y=(xy)z"), + Iteration("x(yz)=2==(xy)z=2", "answer=x(yz)=2", "input=(xy)z=2"), + Iteration("2*(3y)=4==(2y)*3=4", "answer=2*(3y)=4", "input=(2y)*3=4"), + Iteration("x(yz)=(2*3)*4==(xy)z=2*(3*4)", "answer=x(yz)=(2*3)*4", "input=(xy)z=2*(3*4)") + ) + fun testMatches_operationsDiffer_byAssociativity_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Changing operation associativity is allowed by this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y=3.14-1!=y=1-3.14", "answer=y=3.14-1", "input=y=1-3.14"), + Iteration("y=1-(2-3)!=y=(1-2)-3", "answer=y=1-(2-3)", "input=y=(1-2)-3"), + Iteration("y-1=3!=1-y=3", "answer=y-1=3", "input=1-y=3"), + Iteration("x-(y-z)=3!=(x-y)-z=3", "answer=x-(y-z)=3", "input=(x-y)-z=3"), + Iteration("y=3.14/x!=y=x/3.14", "answer=y=3.14/x", "input=y=x/3.14"), + Iteration("y/x=2!=x/y=2", "answer=y/x=2", "input=x/y=2"), + Iteration("y=3.14^2!=y=2^3.14", "answer=y=3.14^2", "input=y=2^3.14"), + Iteration("(3.14^2)y=2!=(2^3.14)y=2", "answer=(3.14^2)y=2", "input=(2^3.14)y=2"), + Iteration("y=x/(y/z)!=y=(x/y)/z", "answer=y=x/(y/z)", "input=y=(x/y)/z"), + Iteration("x/(y/z)=2!=(x/y)/z=2", "answer=x/(y/z)=2", "input=(x/y)/z=2") + ) + fun testMatches_operationsDiffer_byNonCommutativeOrAssociativeReordering_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Non-commutative and non-associative reordering generally results in a different value, so the + // classifier will fail to match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("y=1+2==y=1-(-2)", "answer=y=1+2", "input=y=1-(-2)"), + Iteration("y=1+x==y=1-(-x)", "answer=y=1+x", "input=y=1-(-x)") + ) + fun testMatches_operationsDiffer_byDistributingNegation_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // The classifier does support distributing negations (e.g. a*cross groups). + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y=1-2!=y=-(2-1)", "answer=y=1-2", "input=y=-(2-1)"), + Iteration("z=x-y!=z=-(y-x)", "answer=z=x-y", "input=z=-(y-x)"), + Iteration("y=1+2!=y=1+1+1", "answer=y=1+2", "input=y=1+1+1"), + Iteration("y=4-6!=y=1-2-1", "answer=y=4-6", "input=y=1-2-1"), + Iteration("y=2*3*2*2!=y=2*3*4", "answer=y=2*3*2*2", "input=y=2*3*4"), + Iteration("y=-6-2!=y=2*-(3+1)", "answer=y=-6-2", "input=y=2*-(3+1)"), + Iteration("y=2/3/2/2!=y=2/3/4", "answer=y=2/3/2/2", "input=y=2/3/4"), + Iteration("y=2^(2+1)!=y=2^3", "answer=y=2^(2+1)", "input=y=2^3"), + Iteration("y=2^(-1)!=y=1/2", "answer=y=2^(-1)", "input=y=1/2"), + Iteration("y=2+x!=y=1+x+1", "answer=y=2+x", "input=y=1+x+1"), + Iteration("y=-x!=y=1-x-1", "answer=y=-x", "input=y=1-x-1"), + Iteration("y=4x!=y=2*2*x", "answer=y=4x", "input=y=2*2*x"), + Iteration("y=2-6x!=y=2*(-3x+1)", "answer=y=2-6x", "input=y=2*(-3x+1)"), + Iteration("y=x/4!=y=x/2/2", "answer=y=x/4", "input=y=x/2/2"), + Iteration("y=x^(2+1)!=y=x^3", "answer=y=x^(2+1)", "input=y=x^3"), + Iteration("y=x*(2^(-1))!=y=x/2", "answer=y=x*(2^(-1))", "input=y=x/2"), + Iteration("y+2=x!=1+1+y=x", "answer=y+2=x", "input=1+1+y=x"), + Iteration("(2^2)y=x+2!=4y=x+2", "answer=(2^2)y=x+2", "input=4y=x+2"), + Iteration("y^(4-2)=3x!=y^2=3x", "answer=y^(4-2)=3x", "input=y^2=3x"), + Iteration("y/2/2=3x!=y/4=3x", "answer=y/2/2=3x", "input=y/4=3x") + ) + fun testMatches_operationsDiffer_byDistributionAndCombining_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier doesn't support broadly distributing or combining terms. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("2+x=y!=y=2+x", "answer=2+x=y", "input=y=2+x"), + Iteration("-3x^3=2y^2!=2y^2=-3x^3", "answer=-3x^3=2y^2", "input=2y^2=-3x^3"), + Iteration("-4+x=2+y+1-1!=2+y=x-4", "answer=-4+x=2+y+1-1", "input=2+y=x-4"), + Iteration("y=x-6!=2+y=x-4", "answer=y=x-6", "input=2+y=x-4"), + Iteration("(1+1+1)*x=2*y/4!=y/2=3x", "answer=(1+1+1)*x=2*y/4", "input=y/2=3x") + ) + fun testMatches_sidesRearrangedAroundEqualsSign_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier doesn't support rearranging the left or right-hand sides of the equation. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("y = 3x^2 - 4==y = 3x^2 - 4", "answer=y = 3x^2 - 4", "input=y = 3x^2 - 4"), + Iteration("y = -4 + 3x^2==y = 3x^2 - 4", "answer=y = -4 + 3x^2", "input=y = 3x^2 - 4"), + Iteration("y = x^2*3 - 4==y = 3x^2 - 4", "answer=y = x^2*3 - 4", "input=y = 3x^2 - 4") + ) + fun testMatches_assortedExpressions_withMatchingCharacteristics_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // pass for this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y+4=3x^2!=y = 3x^2 - 4", "answer=y+4=3x^2", "input=y = 3x^2 - 4"), + Iteration("y-3x^2=-4!=y = 3x^2 - 4", "answer=y-3x^2=-4", "input=y = 3x^2 - 4"), + Iteration("-4=y-3x^2!=y = 3x^2 - 4", "answer=-4=y-3x^2", "input=y = 3x^2 - 4"), + Iteration("3x^2-y=4!=y = 3x^2 - 4", "answer=3x^2-y=4", "input=y = 3x^2 - 4"), + Iteration("3x^2=4+y!=y = 3x^2 - 4", "answer=3x^2=4+y", "input=y = 3x^2 - 4"), + Iteration("y-x^2=2x^2-4!=y = 3x^2 - 4", "answer=y-x^2=2x^2-4", "input=y = 3x^2 - 4"), + Iteration("y=x*(2^(1/2))!=y=sqrt(2)x", "answer=y=x*(2^(1/2))", "input=y=sqrt(2)x"), + Iteration("y − 3x^2 = -4!=y = 3x^2 - 4", "answer=y − 3x^2 = -4", "input=y = 3x^2 - 4"), + Iteration("3x^2 - 4 = y!=y = 3x^2 - 4", "answer=3x^2 - 4 = y", "input=y = 3x^2 - 4"), + Iteration("y = 3x^2 - 7!=y = 3x^2 - 4", "answer=y = 3x^2 - 7", "input=y = 3x^2 - 4"), + Iteration("x^2 = 3y - 4!=y = 3x^2 - 4", "answer=x^2 = 3y - 4", "input=y = 3x^2 - 4"), + Iteration("y/(3x^2 - 4) = 1!=y = 3x^2 - 4", "answer=y/(3x^2 - 4) = 1", "input=y = 3x^2 - 4"), + Iteration("y + 3x^2 - 4 = 0!=y = 3x^2 - 4", "answer=y + 3x^2 - 4 = 0", "input=y = 3x^2 - 4"), + Iteration("y − 3x^2 + 4 = 0!=y = 3x^2 - 4", "answer=y − 3x^2 + 4 = 0", "input=y = 3x^2 - 4"), + Iteration("y^2 = 3x^2y - 4y!=y = 3x^2 - 4", "answer=y^2 = 3x^2y - 4y", "input=y = 3x^2 - 4"), + Iteration("y = (3x^3 - 4x)/x!=y = 3x^2 - 4", "answer=y = (3x^3 - 4x)/x", "input=y = 3x^2 - 4"), + Iteration( + "y^2 * y^−1 = -12x^2!=y = 3x^2 - 4", "answer=y^2 * y^−1 = -12x^2", "input=y = 3x^2 - 4" + ), + Iteration("y^2/y = 3x^2 - 4!=y = 3x^2 - 4", "answer=y^2/y = 3x^2 - 4", "input=y = 3x^2 - 4"), + Iteration("2 − 3 = -4!=y = 3x^2 - 4", "answer=2 − 3 = -4", "input=y = 3x^2 - 4"), + Iteration("y = 3x^2 + 4!=y = 3x^2 - 4", "answer=y = 3x^2 + 4", "input=y = 3x^2 - 4"), + Iteration("y - 4 = 3x^2!=y = 3x^2 - 4", "answer=y - 4 = 3x^2", "input=y = 3x^2 - 4"), + Iteration("y − 3x^2 - 4 = 0!=y = 3x^2 - 4", "answer=y − 3x^2 - 4 = 0", "input=y = 3x^2 - 4"), + Iteration("0 = y + 3x^2 - 4!=y = 3x^2 - 4", "answer=0 = y + 3x^2 - 4", "input=y = 3x^2 - 4"), + Iteration("2 = 0!=y = 3x^2 - 4", "answer=2 = 0", "input=y = 3x^2 - 4"), + Iteration("y - x^2 = -4!=y = 3x^2 - 4", "answer=y - x^2 = -4", "input=y = 3x^2 - 4"), + Iteration("y=3x-4!=y = 3x^2 - 4", "answer=y=3x-4", "input=y = 3x^2 - 4"), + Iteration("y/sqrt(2)=x!=y=sqrt(2)x", "answer=y/sqrt(2)=x", "input=y=sqrt(2)x"), + Iteration("y/4=x!=y=4x", "answer=y/4=x", "input=y=4x"), + Iteration("y/4=16x!=y=4x", "answer=y/4=16x", "input=y=4x"), + Iteration("xy=x^2!=y=x", "answer=xy=x^2", "input=y=x") + ) + fun testMatches_assortedExpressions_withoutMatchingCharacteristics_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // not pass for this classifier. + assertThat(matches).isFalse() + } + + private fun matchesClassifier( + answerExpression: InteractionObject, + inputExpression: InteractionObject, + allowedVariables: List = allPossibleVariables + ): Boolean { + return classifier.matches( + answerExpression, + inputs = mapOf("x" to inputExpression), + classificationContext = ClassificationContext( + customizationArgs = mapOf( + "customOskLetters" to SchemaObject.newBuilder().apply { + schemaObjectList = SchemaObjectList.newBuilder().apply { + addAllSchemaObject( + allowedVariables.map { + SchemaObject.newBuilder().setNormalizedString(it).build() + } + ) + }.build() + }.build() + ) + ) + ) + } + + private fun createMathExpression(rawExpression: String) = InteractionObject.newBuilder().apply { + mathExpression = rawExpression + }.build() + + private fun setUpTestApplicationComponent() { + DaggerMathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestDispatcherModule::class, LoggerModule::class, RobolectricModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject( + test: MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest + ) + } +} diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModuleTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModuleTest.kt new file mode 100644 index 00000000000..c3be8b53b1a --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModuleTest.kt @@ -0,0 +1,106 @@ +package org.oppia.android.domain.classify.rules.mathequationinput + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.domain.classify.RuleClassifier +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.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton +import org.oppia.android.domain.classify.rules.MathEquationInputRules + +/** Tests for [MathEquationInputModule]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class MathEquationInputModuleTest { + @field:[Inject MathEquationInputRules] + lateinit var mathEquationInputClassifiers: Map< + String, @JvmSuppressWildcards RuleClassifier> + + @Before + fun setUp() { + setUpTestApplicationComponent() + } + + @Test + fun testModule_hasAtLeastOneClassifier() { + assertThat(mathEquationInputClassifiers).isNotEmpty() + } + + @Test + fun testModule_hasNoDuplicateClassifiers() { + assertThat(mathEquationInputClassifiers.values.toSet()).hasSize( + mathEquationInputClassifiers.size + ) + } + + @Test + fun testModule_providesMatchesExactlyWithClassifier() { + assertThat(mathEquationInputClassifiers).containsKey("MatchesExactlyWith") + } + + @Test + fun testModule_providesMatchesUpToTrivialManipulationsClassifier() { + assertThat(mathEquationInputClassifiers).containsKey("MatchesUpToTrivialManipulations") + } + + @Test + fun testModule_providesIsEquivalentToClassifier() { + assertThat(mathEquationInputClassifiers).containsKey("MatchesExactlyWith") + } + + private fun setUpTestApplicationComponent() { + DaggerMathEquationInputModuleTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestDispatcherModule::class, LoggerModule::class, RobolectricModule::class, + MathEquationInputModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(test: MathEquationInputModuleTest) + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt index 6638a801db0..9b0c5fe9c5c 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt @@ -111,7 +111,9 @@ class OppiaParameterizedTestRunner(private val testClass: Class<*>) : Suite(test " $rawValuePair)" } - val (fieldName, rawValue) = rawValuePair.split('=') + // Use substringBefore/After since values should be allowed to contain '='. + val fieldName = rawValuePair.substringBefore(delimiter = '=') + val rawValue = rawValuePair.substringAfter(delimiter = '=') check(fieldName in fieldsAndParsers) { "Property key does not correspond to any class fields: $fieldName (available:" + " ${fieldsAndParsers.keys})" From 0c00467d00847add59e260539c1e2290cd5ea4bd Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Sat, 5 Feb 2022 00:41:47 -0800 Subject: [PATCH 114/134] Static check & lint fixes. --- ...putIsEquivalentToRuleClassifierProvider.kt | 20 ++++++++++++++----- ...atchesExactlyWithRuleClassifierProvider.kt | 13 +++++++++--- ...vialManipulationsRuleClassifierProvider.kt | 11 +++++++++- ...ManipulationsRuleClassifierProviderTest.kt | 6 ++++-- .../MathEquationInputModuleTest.kt | 2 +- .../file_content_validation_checks.textproto | 3 +++ 6 files changed, 43 insertions(+), 12 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt index de26cf01fea..1bcbdcf21f6 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt @@ -9,13 +9,23 @@ import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation -import org.oppia.android.util.math.toPolynomial -import javax.inject.Inject import org.oppia.android.util.math.isApproximatelyEqualTo import org.oppia.android.util.math.minus import org.oppia.android.util.math.sort +import org.oppia.android.util.math.toPolynomial import org.oppia.android.util.math.unaryMinus +import javax.inject.Inject +/** + * Provider for a classifier that determines whether a math equation expression is mathematically + * equivalent to the creator-specific equation defined as the input to this interaction. + * + * Note that both equations are assumed and parsed as polynomial equations. Furthermore, this + * classifier allows the two sides of the equations to be rearranged in any way on either side of + * the '=' sign (but they can't be multiples of each other). + * + * See this class's tests for a list of supported cases (both for matching and not matching). + */ class MathEquationInputIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, private val consoleLogger: ConsoleLogger @@ -48,9 +58,9 @@ class MathEquationInputIsEquivalentToRuleClassifierProvider @Inject constructor( // arranged. Furthermore, the '-1' check is correct since the order of the equation can flip // depending on how it was inputted, and '-1 * 0=0' so the new right-hand side remains // unaffected. - return newAnswerLhs.isApproximatelyEqualTo(newInputLhs) - || negativeAnswerLhs.isApproximatelyEqualTo(newInputLhs) - || newAnswerLhs.isApproximatelyEqualTo(negativeInputLhs) + return newAnswerLhs.isApproximatelyEqualTo(newInputLhs) || + negativeAnswerLhs.isApproximatelyEqualTo(newInputLhs) || + newAnswerLhs.isApproximatelyEqualTo(negativeInputLhs) } private fun parsePolynomials( diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt index 8cbf7b08b96..6ea32f60e48 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt @@ -9,9 +9,16 @@ import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation -import javax.inject.Inject import org.oppia.android.util.math.isApproximatelyEqualTo +import javax.inject.Inject +/** + * Provider for a classifier that determines whether a math equation is exactly equal to the + * creator-specific equation defined as the input to this interaction, including any parenthetical + * groups in the equations and operand order. + * + * See this class's tests for a list of supported cases (both for matching and not matching). + */ class MathEquationInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, private val consoleLogger: ConsoleLogger @@ -62,8 +69,8 @@ class MathEquationInputMatchesExactlyWithRuleClassifierProvider @Inject construc } private fun MathEquation.approximatelyEquals(other: MathEquation): Boolean { - return leftSide.isApproximatelyEqualTo(other.leftSide) - && rightSide.isApproximatelyEqualTo(other.rightSide) + return leftSide.isApproximatelyEqualTo(other.leftSide) && + rightSide.isApproximatelyEqualTo(other.rightSide) } } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index 207161b7c8d..db463880ab0 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -9,10 +9,19 @@ import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation +import org.oppia.android.util.math.isApproximatelyEqualTo import org.oppia.android.util.math.toComparableOperation import javax.inject.Inject -import org.oppia.android.util.math.isApproximatelyEqualTo +/** + * Provider for a classifier that determines whether a math equation is equal to the + * creator-specific equation defined as the input to this interaction, with some manipulations. + * + * 'Trivial manipulations' indicates rearranging any operands for commutative operations, or changes + * in resolution order (i.e. associative) without changing the meaning of the equation. + * + * See this class's tests for a list of supported cases (both for matching and not matching). + */ class MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt index 604ab230cb3..9af344ecb91 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt @@ -31,6 +31,8 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton +import org.oppia.android.domain.classify.rules.mathequationinput.DaggerMathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent as DaggerTestApplicationComponent +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider as RuleClassifierProvider /** * Tests for [MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider]. @@ -46,7 +48,7 @@ import javax.inject.Singleton @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest { - @Inject internal lateinit var provider: MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider + @Inject internal lateinit var provider: RuleClassifierProvider @Parameter lateinit var answer: String @Parameter lateinit var input: String @@ -373,7 +375,7 @@ class MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest }.build() private fun setUpTestApplicationComponent() { - DaggerMathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent + DaggerTestApplicationComponent .builder() .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModuleTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModuleTest.kt index c3be8b53b1a..4958d6d0fb4 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModuleTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModuleTest.kt @@ -13,6 +13,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.MathEquationInputRules import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule @@ -22,7 +23,6 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.domain.classify.rules.MathEquationInputRules /** Tests for [MathEquationInputModule]. */ // FunctionName: test names are conventionally named with underscores. diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index 03c575af12c..70a2d35df7a 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -289,6 +289,9 @@ file_content_checks { exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt" + exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProviderTest.kt" + exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProviderTest.kt" + exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt" From d026506cf487086dbfb7a90e8b0d5e52b077ddc2 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Sat, 5 Feb 2022 02:00:53 -0800 Subject: [PATCH 115/134] Post-merge fixes. Verified CI checks & all unit tests are passing. --- .../AdministratorControlsFragmentTest.kt | 7 ++++++- .../android/app/mydownloads/MyDownloadsActivityTest.kt | 7 ++++++- .../app/settings/profile/ProfileResetPinFragmentTest.kt | 6 +++++- .../oppia/android/app/parser/FractionParsingUiErrorTest.kt | 7 ++++++- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsFragmentTest.kt index a80d9166392..b4cd0fe29fe 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsFragmentTest.kt @@ -39,13 +39,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -174,7 +177,9 @@ class AdministratorControlsFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsActivityTest.kt index 4ac6a451c69..e609ec731b7 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsActivityTest.kt @@ -28,13 +28,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -130,7 +133,9 @@ class MyDownloadsActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentTest.kt index 4308d16cfd3..d54d1ee4c83 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentTest.kt @@ -47,13 +47,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -1015,7 +1018,8 @@ class ProfileResetPinFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { 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 index 0342d7e3539..54f532fd57f 100644 --- a/app/src/test/java/org/oppia/android/app/parser/FractionParsingUiErrorTest.kt +++ b/app/src/test/java/org/oppia/android/app/parser/FractionParsingUiErrorTest.kt @@ -27,13 +27,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -241,7 +244,9 @@ class FractionParsingUiErrorTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { From c4d53ddcad0859ebf6e153cb6f583a241d57ee90 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Sat, 5 Feb 2022 02:36:06 -0800 Subject: [PATCH 116/134] Split up tests. Also, adds dedicated BUILD.bazel file for new test. --- .../android/app/utility/math/BUILD.bazel | 30 + .../MathExpressionAccessibilityUtilTest.kt | 838 ++++++++++++------ 2 files changed, 598 insertions(+), 270 deletions(-) create mode 100644 app/src/test/java/org/oppia/android/app/utility/math/BUILD.bazel diff --git a/app/src/test/java/org/oppia/android/app/utility/math/BUILD.bazel b/app/src/test/java/org/oppia/android/app/utility/math/BUILD.bazel new file mode 100644 index 00000000000..efbfba1af52 --- /dev/null +++ b/app/src/test/java/org/oppia/android/app/utility/math/BUILD.bazel @@ -0,0 +1,30 @@ +""" +Tests for UI-specific math utilities. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("//:oppia_android_test.bzl", "oppia_android_test") + +oppia_android_test( + name = "MathExpressionAccessibilityUtilTest", + srcs = ["MathExpressionAccessibilityUtilTest.kt"], + custom_package = "org.oppia.android.app.utility.math", + test_class = "org.oppia.android.app.utility.math.MathExpressionAccessibilityUtilTest", + test_manifest = "//app:test_manifest", + deps = [ + ":dagger", + "//app/src/main/java/org/oppia/android/app/utility/math:math_expression_accessibility_util", + "//model/src/main/proto:languages_java_proto_lite", + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", + "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", + "//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:math_expression_parser", + ], +) + +dagger_rules() diff --git a/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt b/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt index 6ada50c67ff..c536a5b8c03 100644 --- a/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt +++ b/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt @@ -1,7 +1,6 @@ package org.oppia.android.app.utility.math import android.app.Application -import androidx.appcompat.app.AppCompatActivity import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.StringSubject @@ -9,20 +8,11 @@ import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import dagger.BindsInstance import dagger.Component -import dagger.Module -import dagger.Provides +import javax.inject.Inject +import javax.inject.Singleton import org.junit.Before 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.MathEquation import org.oppia.android.app.model.MathExpression import org.oppia.android.app.model.OppiaLanguage @@ -34,70 +24,25 @@ import org.oppia.android.app.model.OppiaLanguage.HINGLISH import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED -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.algebraicexpressioninput.AlgebraicExpressionInputModule -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.mathequationinput.MathEquationInputModule -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.numericexpressioninput.NumericExpressionInputModule -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.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.math.MathEquationSubject import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat import org.oppia.android.testing.math.MathExpressionSubject import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat -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.EnableConsoleLog -import org.oppia.android.util.logging.EnableFileLog -import org.oppia.android.util.logging.GlobalLogLevel -import org.oppia.android.util.logging.LogLevel import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult -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.Inject -import javax.inject.Singleton /** Tests for [MathExpressionAccessibilityUtil]. */ @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = MathExpressionAccessibilityUtilTest.TestApplication::class) class MathExpressionAccessibilityUtilTest { - @Inject lateinit var util: MathExpressionAccessibilityUtil + @Inject + lateinit var util: MathExpressionAccessibilityUtil + + // TODO: finish tests @Before fun setUp() { @@ -105,113 +50,274 @@ class MathExpressionAccessibilityUtilTest { } @Test - fun testHumanReadableString() { - // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). - - val exp1 = parseNumericExpressionSuccessfullyWithAllErrors("1") - assertThat(exp1).forHumanReadable(ARABIC).doesNotConvertToString() + fun test1() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp).forHumanReadable(ARABIC).doesNotConvertToString() + } - assertThat(exp1).forHumanReadable(HINDI).doesNotConvertToString() + @Test + fun test2() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp).forHumanReadable(HINDI).doesNotConvertToString() + } - assertThat(exp1).forHumanReadable(HINGLISH).doesNotConvertToString() + @Test + fun test3() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp).forHumanReadable(HINGLISH).doesNotConvertToString() + } - assertThat(exp1).forHumanReadable(PORTUGUESE).doesNotConvertToString() + @Test + fun test4() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp).forHumanReadable(PORTUGUESE).doesNotConvertToString() + } - assertThat(exp1).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + @Test + fun test5() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + } - assertThat(exp1).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + @Test + fun test6() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + } - assertThat(exp1).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + @Test + fun test7() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + } - val exp2 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") - assertThat(exp2).forHumanReadable(ARABIC).doesNotConvertToString() + @Test + fun test8() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp).forHumanReadable(ARABIC).doesNotConvertToString() + } - assertThat(exp2).forHumanReadable(HINDI).doesNotConvertToString() + @Test + fun test9() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp).forHumanReadable(HINDI).doesNotConvertToString() + } - assertThat(exp2).forHumanReadable(HINGLISH).doesNotConvertToString() + @Test + fun test10() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp).forHumanReadable(HINGLISH).doesNotConvertToString() + } - assertThat(exp2).forHumanReadable(PORTUGUESE).doesNotConvertToString() + @Test + fun test11() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp).forHumanReadable(PORTUGUESE).doesNotConvertToString() + } - assertThat(exp2).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + @Test + fun test12() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + } - assertThat(exp2).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + @Test + fun test13() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + } - assertThat(exp2).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + @Test + fun test14() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + } - val eq1 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") - assertThat(eq1).forHumanReadable(ARABIC).doesNotConvertToString() + @Test + fun test15() { + // TODO: do something with this test. + val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") + assertThat(eq).forHumanReadable(ARABIC).doesNotConvertToString() + } - assertThat(eq1).forHumanReadable(HINDI).doesNotConvertToString() + @Test + fun test16() { + // TODO: do something with this test. + val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") + assertThat(eq).forHumanReadable(HINDI).doesNotConvertToString() + } - assertThat(eq1).forHumanReadable(HINGLISH).doesNotConvertToString() + @Test + fun test17() { + // TODO: do something with this test. + val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") + assertThat(eq).forHumanReadable(HINGLISH).doesNotConvertToString() + } - assertThat(eq1).forHumanReadable(PORTUGUESE).doesNotConvertToString() + @Test + fun test18() { + // TODO: do something with this test. + val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") + assertThat(eq).forHumanReadable(PORTUGUESE).doesNotConvertToString() + } - assertThat(eq1).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + @Test + fun test19() { + // TODO: do something with this test. + val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") + assertThat(eq).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + } - assertThat(eq1).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + @Test + fun test20() { + // TODO: do something with this test. + val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") + assertThat(eq).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + } - assertThat(eq1).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + @Test + fun test21() { + // TODO: do something with this test. + val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") + assertThat(eq).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + } + @Test + fun test22() { + // TODO: do something with this test. // specific cases (from rules & other cases): - val exp3 = parseNumericExpressionSuccessfullyWithAllErrors("1") - assertThat(exp3).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") - assertThat(exp3).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + } - val exp49 = parseNumericExpressionSuccessfullyWithAllErrors("-1") - assertThat(exp49).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("negative 1") + @Test + fun test23() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("-1") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("negative 1") + } - val exp50 = parseNumericExpressionSuccessfullyWithAllErrors("+1") - assertThat(exp50).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("positive 1") + @Test + fun test24() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithoutOptionalErrors("+1") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("positive 1") + } - val exp4 = parseNumericExpressionSuccessfullyWithoutOptionalErrors("((1))") - assertThat(exp4).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + @Test + fun test25() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithoutOptionalErrors("((1))") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + } - val exp5 = parseNumericExpressionSuccessfullyWithAllErrors("1+2") - assertThat(exp5).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 plus 2") + @Test + fun test26() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+2") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 plus 2") + } - val exp6 = parseNumericExpressionSuccessfullyWithAllErrors("1-2") - assertThat(exp6).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 minus 2") + @Test + fun test27() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1-2") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 minus 2") + } - val exp7 = parseNumericExpressionSuccessfullyWithAllErrors("1*2") - assertThat(exp7).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 times 2") + @Test + fun test28() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1*2") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 times 2") + } - val exp8 = parseNumericExpressionSuccessfullyWithAllErrors("1/2") - assertThat(exp8).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 divided by 2") + @Test + fun test29() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1/2") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 divided by 2") + } - val exp9 = parseNumericExpressionSuccessfullyWithAllErrors("1+(1-2)") - assertThat(exp9) + @Test + fun test30() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+(1-2)") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("1 plus open parenthesis 1 minus 2 close parenthesis") + } - val exp10 = parseNumericExpressionSuccessfullyWithAllErrors("2^3") - assertThat(exp10) + @Test + fun test31() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("2^3") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("2 raised to the power of 3") + } - val exp11 = parseNumericExpressionSuccessfullyWithAllErrors("2^(1+2)") - assertThat(exp11) + @Test + fun test32() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("2^(1+2)") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("2 raised to the power of open parenthesis 1 plus 2 close parenthesis") + } - val exp12 = parseNumericExpressionSuccessfullyWithAllErrors("100000*2") - assertThat(exp12).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("100,000 times 2") + @Test + fun test33() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("100000*2") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("100,000 times 2") + } - val exp13 = parseNumericExpressionSuccessfullyWithAllErrors("sqrt(2)") - assertThat(exp13).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + @Test + fun test34() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("sqrt(2)") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + } - val exp14 = parseNumericExpressionSuccessfullyWithAllErrors("√2") - assertThat(exp14).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + @Test + fun test35() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("√2") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + } - val exp15 = parseNumericExpressionSuccessfullyWithAllErrors("sqrt(1+2)") - assertThat(exp15) + @Test + fun test36() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("sqrt(1+2)") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("start square root 1 plus 2 end square root") + } + @Test + fun test37() { + // TODO: do something with this test. val singularOrdinalNames = mapOf( 1 to "oneth", 2 to "half", @@ -238,296 +344,528 @@ class MathExpressionAccessibilityUtilTest { ) for (denominatorToCheck in 1..10) { for (numeratorToCheck in 0..denominatorToCheck) { - val exp16 = + val exp = parseNumericExpressionSuccessfullyWithAllErrors("$numeratorToCheck/$denominatorToCheck") val ordinalName = if (numeratorToCheck == 1) { singularOrdinalNames.getValue(denominatorToCheck) } else pluralOrdinalNames.getValue(denominatorToCheck) - assertThat(exp16) + assertThat(exp) .forHumanReadable(ENGLISH) .convertsWithFractionsToStringThat() .isEqualTo("$numeratorToCheck $ordinalName") } } + } - val exp17 = parseNumericExpressionSuccessfullyWithAllErrors("-1/3") - assertThat(exp17) + @Test + fun test38() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("-1/3") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsWithFractionsToStringThat() .isEqualTo("negative 1 third") + } - val exp18 = parseNumericExpressionSuccessfullyWithAllErrors("-2/3") - assertThat(exp18) + @Test + fun test39() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("-2/3") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsWithFractionsToStringThat() .isEqualTo("negative 2 thirds") + } - val exp19 = parseNumericExpressionSuccessfullyWithAllErrors("10/11") - assertThat(exp19) + @Test + fun test40() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("10/11") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsWithFractionsToStringThat() .isEqualTo("10 over 11") + } - val exp20 = parseNumericExpressionSuccessfullyWithAllErrors("121/7986") - assertThat(exp20) + @Test + fun test41() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("121/7986") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsWithFractionsToStringThat() .isEqualTo("121 over 7,986") + } - val exp21 = parseNumericExpressionSuccessfullyWithAllErrors("8/7") - assertThat(exp21) + @Test + fun test42() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("8/7") + assertThat(exp) .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() + .convertsToStringThat() .isEqualTo("8 over 7") + } - val exp22 = parseNumericExpressionSuccessfullyWithAllErrors("-10/-30") - assertThat(exp22) + @Test + fun test43() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("-10/-30") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsWithFractionsToStringThat() .isEqualTo("negative the fraction with numerator 10 and denominator negative 30") + } - val exp23 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1") - assertThat(exp23).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + @Test + fun test44() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + } - val exp24 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("((1))") - assertThat(exp24).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + @Test + fun test45() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("((1))") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + } - val exp25 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") - assertThat(exp25).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x") + @Test + fun test46() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x") + } - val exp26 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("((x))") - assertThat(exp26).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x") + @Test + fun test47() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("((x))") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x") + } - val exp51 = parseAlgebraicExpressionSuccessfullyWithAllErrors("-x") - assertThat(exp51).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("negative x") + @Test + fun test48() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("-x") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("negative x") + } - val exp52 = parseAlgebraicExpressionSuccessfullyWithAllErrors("+x") - assertThat(exp52).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("positive x") + @Test + fun test49() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("+x") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("positive x") + } - val exp27 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x") - assertThat(exp27).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 plus x") + @Test + fun test50() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 plus x") + } - val exp28 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1-x") - assertThat(exp28).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 minus x") + @Test + fun test51() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1-x") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 minus x") + } - val exp29 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1*x") - assertThat(exp29).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 times x") + @Test + fun test52() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1*x") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 times x") + } - val exp30 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1/x") - assertThat(exp30).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 divided by x") + @Test + fun test53() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1/x") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 divided by x") + } - val exp31 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1/x") - assertThat(exp31) + @Test + fun test54() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1/x") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsWithFractionsToStringThat() .isEqualTo("the fraction with numerator 1 and denominator x") + } - val exp32 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(1-x)") - assertThat(exp32) + @Test + fun test55() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(1-x)") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("1 plus open parenthesis 1 minus x close parenthesis") + } - val exp33 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2x") - assertThat(exp33).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("2 x") + @Test + fun test56() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2x") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("2 x") + } - val exp34 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy") - assertThat(exp34).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x times y") + @Test + fun test57() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x times y") + } - val exp35 = parseAlgebraicExpressionSuccessfullyWithAllErrors("z") - assertThat(exp35).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("zed") + @Test + fun test58() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("z") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("zed") + } - val exp36 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2xz") - assertThat(exp36).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("2 x times zed") + @Test + fun test59() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2xz") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("2 x times zed") + } - val exp37 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2") - assertThat(exp37) + @Test + fun test60() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("x raised to the power of 2") + } - val exp38 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("x^(1+x)") - assertThat(exp38) + @Test + fun test61() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("x^(1+x)") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("x raised to the power of open parenthesis 1 plus x close parenthesis") + } - val exp39 = parseAlgebraicExpressionSuccessfullyWithAllErrors("100000*2") - assertThat(exp39).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("100,000 times 2") + @Test + fun test62() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("100000*2") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("100,000 times 2") + } - val exp40 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(2)") - assertThat(exp40).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + @Test + fun test63() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(2)") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + } - val exp41 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(x)") - assertThat(exp41).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of x") + @Test + fun test64() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(x)") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of x") + } - val exp42 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√2") - assertThat(exp42).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + @Test + fun test65() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("√2") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + } - val exp43 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√x") - assertThat(exp43).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of x") + @Test + fun test66() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("√x") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of x") + } - val exp44 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(1+2)") - assertThat(exp44) + @Test + fun test67() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(1+2)") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("start square root 1 plus 2 end square root") + } - val exp45 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(1+x)") - assertThat(exp45) + @Test + fun test68() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(1+x)") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("start square root 1 plus x end square root") + } - val exp46 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√(1+x)") - assertThat(exp46) + @Test + fun test69() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("√(1+x)") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("start square root open parenthesis 1 plus x close parenthesis end square root") + } + @Test + fun test70() { + // TODO: do something with this test. + val singularOrdinalNames = mapOf( + 1 to "oneth", + 2 to "half", + 3 to "third", + 4 to "fourth", + 5 to "fifth", + 6 to "sixth", + 7 to "seventh", + 8 to "eighth", + 9 to "ninth", + 10 to "tenth", + ) + val pluralOrdinalNames = mapOf( + 1 to "oneths", + 2 to "halves", + 3 to "thirds", + 4 to "fourths", + 5 to "fifths", + 6 to "sixths", + 7 to "sevenths", + 8 to "eighths", + 9 to "ninths", + 10 to "tenths", + ) for (denominatorToCheck in 1..10) { for (numeratorToCheck in 0..denominatorToCheck) { - val exp16 = + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("$numeratorToCheck/$denominatorToCheck") val ordinalName = if (numeratorToCheck == 1) { singularOrdinalNames.getValue(denominatorToCheck) } else pluralOrdinalNames.getValue(denominatorToCheck) - assertThat(exp16) + assertThat(exp) .forHumanReadable(ENGLISH) .convertsWithFractionsToStringThat() .isEqualTo("$numeratorToCheck $ordinalName") } } + } - val exp47 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1") - assertThat(exp47).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + @Test + fun test71() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + } - val exp48 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x(5-y)") - assertThat(exp48) + @Test + fun test72() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x(5-y)") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("x times open parenthesis 5 minus y close parenthesis") + } - val eq2 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/y") - assertThat(eq2) + @Test + fun test73() { + // TODO: do something with this test. + val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/y") + assertThat(eq) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("x equals 1 divided by y") + } - val eq3 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/2") - assertThat(eq3) + @Test + fun test74() { + // TODO: do something with this test. + val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/2") + assertThat(eq) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("x equals 1 divided by 2") + } - val eq4 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/y") - assertThat(eq4) + @Test + fun test75() { + // TODO: do something with this test. + val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/y") + assertThat(eq) .forHumanReadable(ENGLISH) .convertsWithFractionsToStringThat() .isEqualTo("x equals the fraction with numerator 1 and denominator y") + } - val eq5 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/2") - assertThat(eq5) + @Test + fun test76() { + // TODO: do something with this test. + val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/2") + assertThat(eq) .forHumanReadable(ENGLISH) .convertsWithFractionsToStringThat() .isEqualTo("x equals 1 half") + } + @Test + fun test77() { + // TODO: do something with this test. // Tests from examples in the PRD - val eq6 = parseAlgebraicEquationSuccessfullyWithAllErrors("3x^2+4y=62") - assertThat(eq6) + val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("3x^2+4y=62") + assertThat(eq) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("3 x raised to the power of 2 plus 4 y equals 62") + } - val exp53 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x+6)/(x-4)") - assertThat(exp53) + @Test + fun test78() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x+6)/(x-4)") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsWithFractionsToStringThat() .isEqualTo( "the fraction with numerator open parenthesis x plus 6 close parenthesis and denominator" + " open parenthesis x minus 4 close parenthesis" ) + } - val exp54 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("4*(x)^(2)+20x") - assertThat(exp54) + @Test + fun test79() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("4*(x)^(2)+20x") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("4 times x raised to the power of 2 plus 20 x") + } - val exp55 = parseAlgebraicExpressionSuccessfullyWithAllErrors("3+x-5") - assertThat(exp55).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("3 plus x minus 5") + @Test + fun test80() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("3+x-5") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("3 plus x minus 5") + } - val exp56 = + @Test + fun test81() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors( "Z+A-Z", allowedVariables = listOf("A", "Z") ) - assertThat(exp56).forHumanReadable(ENGLISH) + assertThat(exp).forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("Zed plus A minus Zed") + } - val exp57 = + @Test + fun test82() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors( "6C-5A-1", allowedVariables = listOf("A", "C") ) - assertThat(exp57) + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("6 C minus 5 A minus 1") + } - val exp58 = + @Test + fun test83() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors( "5*Z-w", allowedVariables = listOf("Z", "w") ) - assertThat(exp58) + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("5 times Zed minus w") + } - val exp59 = + @Test + fun test84() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors( "L*S-3S+L", allowedVariables = listOf("L", "S") ) - assertThat(exp59) + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("L times S minus 3 S plus L") + } - val exp60 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(2+6+3+4)") - assertThat(exp60) + @Test + fun test85() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(2+6+3+4)") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("2 times open parenthesis 2 plus 6 plus 3 plus 4 close parenthesis") + } - val exp61 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(64)") - assertThat(exp61) + @Test + fun test86() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(64)") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("square root of 64") + } - val exp62 = + @Test + fun test87() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors( "√(a+b)", allowedVariables = listOf("a", "b") ) - assertThat(exp62) + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("start square root open parenthesis a plus b close parenthesis end square root") + } - val exp63 = parseAlgebraicExpressionSuccessfullyWithAllErrors("3*10^-5") - assertThat(exp63) + @Test + fun test88() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("3*10^-5") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("3 times 10 raised to the power of negative 5") + } - val exp64 = + @Test + fun test89() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors( "((x+2y)+5*(a-2b)+z)", allowedVariables = listOf("x", "y", "a", "b", "z") ) - assertThat(exp64) + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo( @@ -582,49 +920,13 @@ class MathExpressionAccessibilityUtilTest { } } - // TODO(#89): Move this to a common test application component. - @Module - class TestModule { - // TODO(#59): Either isolate these to their own shared test module, or use the real logging - // module in tests to avoid needing to specify these settings for tests. - @EnableConsoleLog - @Provides - fun provideEnableConsoleLog(): Boolean = true - - @EnableFileLog - @Provides - fun provideEnableFileLog(): Boolean = false - - @GlobalLogLevel - @Provides - fun provideGlobalLogLevel(): LogLevel = LogLevel.VERBOSE - } - // TODO(#89): Move this to a common test application component. @Singleton @Component( modules = [ - TestModule::class, RobolectricModule::class, FakeOppiaClockModule::class, - TestLogReportingModule::class, TestDispatcherModule::class, ApplicationModule::class, - ApplicationStartupListenerModule::class, WorkManagerConfigurationModule::class, - ImageParsingModule::class, AccessibilityTestModule::class, PracticeTabModule::class, - GcsResourceModule::class, NetworkConnectionUtilDebugModule::class, LogStorageModule::class, - NetworkModule::class, PlatformParameterModule::class, HintsAndSolutionProdModule::class, - CachingTestModule::class, InteractionsModule::class, ExplorationStorageModule::class, - QuestionModule::class, NetworkConfigProdModule::class, ContinueModule::class, - FractionInputModule::class, ItemSelectionInputModule::class, MultipleChoiceInputModule::class, - NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, - DragDropSortInputModule::class, ImageClickInputModule::class, RatioInputModule::class, - HintsAndSolutionConfigModule::class, ExpirationMetaDataRetrieverModule::class, - GlideImageLoaderModule::class, PrimeTopicAssetsControllerModule::class, - HtmlParserEntityTypeModule::class, NetworkConnectionDebugUtilModule::class, - DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, AssetModule::class, - LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class, NumericExpressionInputModule::class, - AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) - interface TestApplicationComponent : ApplicationComponent { + interface TestApplicationComponent { @Component.Builder interface Builder { @BindsInstance @@ -636,7 +938,7 @@ class MathExpressionAccessibilityUtilTest { fun inject(test: MathExpressionAccessibilityUtilTest) } - class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + class TestApplication : Application() { private val component: TestApplicationComponent by lazy { DaggerMathExpressionAccessibilityUtilTest_TestApplicationComponent.builder() .setApplication(this) @@ -646,15 +948,11 @@ class MathExpressionAccessibilityUtilTest { fun inject(test: MathExpressionAccessibilityUtilTest) { component.inject(test) } - - override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { - return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() - } - - override fun getApplicationInjector(): ApplicationInjector = component } private companion object { + // TODO: finalize this API. + private fun parseNumericExpressionSuccessfullyWithAllErrors( expression: String ): MathExpression { From 15d9d25baa8602d15366e69e1bf763ea9347ceb4 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 7 Feb 2022 13:09:12 -0800 Subject: [PATCH 117/134] Add missing test in Bazel, and fix it. --- .../oppia/android/app/databinding/BUILD.bazel | 42 +++++++++++++++++++ .../DrawableBindingAdaptersTest.kt | 7 +++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/BUILD.bazel b/app/src/sharedTest/java/org/oppia/android/app/databinding/BUILD.bazel index 64cac000417..26bfefccfd9 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/BUILD.bazel +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/BUILD.bazel @@ -39,6 +39,48 @@ genrule( """, ) +genrule( + name = "update_DrawableBindingAdaptersTest", + srcs = ["DrawableBindingAdaptersTest.kt"], + outs = ["DrawableBindingAdaptersTest_updated.kt"], + cmd = """ + cat $(SRCS) | + sed 's/import org.oppia.android.R/import org.oppia.android.app.databinding.R/g' | + sed 's/import org.oppia.android.databinding./import org.oppia.android.app.databinding.databinding./g' | + sed 's/import org.oppia.android.app.databinding.DrawableBindingAdapters./import org.oppia.android.app.databinding.DrawableBindingAdapters_updated./g' > $(OUTS) + """, +) + +oppia_android_test( + name = "DrawableBindingAdaptersTest", + srcs = ["DrawableBindingAdaptersTest_updated.kt"], + custom_package = "org.oppia.android.app.databinding", + test_class = "org.oppia.android.app.databinding.DrawableBindingAdaptersTest", + test_manifest = "//app:test_manifest", + deps = [ + ":dagger", + "//app", + "//app:test_deps", + "//app/src/main/java/org/oppia/android/app/activity:activity_intent_factories_shim", + "//app/src/main/java/org/oppia/android/app/translation/testing:test_module", + "//domain", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/junit:initialize_default_locale_rule", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_ext_junit", + "//third_party:androidx_test_ext_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/accessibility:test_module", + "//utility/src/main/java/org/oppia/android/util/caching/testing:caching_test_module", + "//utility/src/main/java/org/oppia/android/util/data:data_providers", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + oppia_android_test( name = "ImageViewBindingAdaptersTest", srcs = ["ImageViewBindingAdaptersTest_updated.kt"], diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/DrawableBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/DrawableBindingAdaptersTest.kt index 0cc4311d565..11b533cb940 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/DrawableBindingAdaptersTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/DrawableBindingAdaptersTest.kt @@ -38,13 +38,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -171,7 +174,9 @@ class DrawableBindingAdaptersTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { From b63883e2fd3d4fe17fc0aa619d7342b38f52b58b Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 7 Feb 2022 13:10:14 -0800 Subject: [PATCH 118/134] Correct order for genrule. --- .../oppia/android/app/databinding/BUILD.bazel | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/BUILD.bazel b/app/src/sharedTest/java/org/oppia/android/app/databinding/BUILD.bazel index 26bfefccfd9..341b24b5d13 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/BUILD.bazel +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/BUILD.bazel @@ -28,26 +28,26 @@ _TEST_FILES = [ ] genrule( - name = "update_ImageViewBindingAdaptersTest", - srcs = ["ImageViewBindingAdaptersTest.kt"], - outs = ["ImageViewBindingAdaptersTest_updated.kt"], + name = "update_DrawableBindingAdaptersTest", + srcs = ["DrawableBindingAdaptersTest.kt"], + outs = ["DrawableBindingAdaptersTest_updated.kt"], cmd = """ cat $(SRCS) | sed 's/import org.oppia.android.R/import org.oppia.android.app.databinding.R/g' | sed 's/import org.oppia.android.databinding./import org.oppia.android.app.databinding.databinding./g' | - sed 's/import org.oppia.android.app.databinding.ImageViewBindingAdapters./import org.oppia.android.app.databinding.ImageViewBindingAdapters_updated./g' > $(OUTS) + sed 's/import org.oppia.android.app.databinding.DrawableBindingAdapters./import org.oppia.android.app.databinding.DrawableBindingAdapters_updated./g' > $(OUTS) """, ) genrule( - name = "update_DrawableBindingAdaptersTest", - srcs = ["DrawableBindingAdaptersTest.kt"], - outs = ["DrawableBindingAdaptersTest_updated.kt"], + name = "update_ImageViewBindingAdaptersTest", + srcs = ["ImageViewBindingAdaptersTest.kt"], + outs = ["ImageViewBindingAdaptersTest_updated.kt"], cmd = """ cat $(SRCS) | sed 's/import org.oppia.android.R/import org.oppia.android.app.databinding.R/g' | sed 's/import org.oppia.android.databinding./import org.oppia.android.app.databinding.databinding./g' | - sed 's/import org.oppia.android.app.databinding.DrawableBindingAdapters./import org.oppia.android.app.databinding.DrawableBindingAdapters_updated./g' > $(OUTS) + sed 's/import org.oppia.android.app.databinding.ImageViewBindingAdapters./import org.oppia.android.app.databinding.ImageViewBindingAdapters_updated./g' > $(OUTS) """, ) From 06427575cdbfbd5ab2e2505d9c7598ad63c5cac2 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 7 Feb 2022 18:52:23 -0800 Subject: [PATCH 119/134] Add full test suite. --- .../math/MathExpressionAccessibilityUtil.kt | 87 +- .../android/app/utility/math/BUILD.bazel | 4 +- .../MathExpressionAccessibilityUtilTest.kt | 1774 ++++++++++------- 3 files changed, 1060 insertions(+), 805 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt b/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt index c353b625048..efd658d12b1 100644 --- a/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt +++ b/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt @@ -30,33 +30,37 @@ import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED import org.oppia.android.app.model.Real.RealTypeCase.INTEGER -import org.oppia.android.util.math.toPlainText import java.text.NumberFormat import java.util.Locale import javax.inject.Inject import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator +import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL +import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL +import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET class MathExpressionAccessibilityUtil @Inject constructor() { + // TODO: document that rationals aren't supported, and that irrationals are rounded during formatting (and maybe that ints are also formatted?). + fun convertToHumanReadableString( - equation: MathEquation, + expression: MathExpression, language: OppiaLanguage, divAsFraction: Boolean ): String? { return when (language) { - ENGLISH -> equation.toHumanReadableEnglishString(divAsFraction) + ENGLISH -> expression.toHumanReadableEnglishString(divAsFraction) ARABIC, HINDI, HINGLISH, PORTUGUESE, BRAZILIAN_PORTUGUESE, LANGUAGE_UNSPECIFIED, UNRECOGNIZED -> null } } fun convertToHumanReadableString( - expression: MathExpression, + equation: MathEquation, language: OppiaLanguage, divAsFraction: Boolean ): String? { return when (language) { - ENGLISH -> expression.toHumanReadableEnglishString(divAsFraction) + ENGLISH -> equation.toHumanReadableEnglishString(divAsFraction) ARABIC, HINDI, HINGLISH, PORTUGUESE, BRAZILIAN_PORTUGUESE, LANGUAGE_UNSPECIFIED, UNRECOGNIZED -> null } @@ -65,30 +69,6 @@ class MathExpressionAccessibilityUtil @Inject constructor() { private companion object { // TODO: move these to the UI layer & have them utilize non-translatable strings. private val numberFormat by lazy { NumberFormat.getNumberInstance(Locale.US) } - private val singularOrdinalNames = mapOf( - 1 to "oneth", - 2 to "half", - 3 to "third", - 4 to "fourth", - 5 to "fifth", - 6 to "sixth", - 7 to "seventh", - 8 to "eighth", - 9 to "ninth", - 10 to "tenth", - ) - private val pluralOrdinalNames = mapOf( - 1 to "oneths", - 2 to "halves", - 3 to "thirds", - 4 to "fourths", - 5 to "fifths", - 6 to "sixths", - 7 to "sevenths", - 8 to "eighths", - 9 to "ninths", - 10 to "tenths", - ) private fun MathEquation.toHumanReadableEnglishString(divAsFraction: Boolean): String? { val lhsStr = leftSide.toHumanReadableEnglishString(divAsFraction) @@ -100,9 +80,13 @@ class MathExpressionAccessibilityUtil @Inject constructor() { // Reference: // https://docs.google.com/document/d/1P-dldXQ08O-02ZRG978paiWOSz0dsvcKpDgiV_rKH_Y/view. return when (expressionTypeCase) { - CONSTANT -> if (constant.realTypeCase == INTEGER) { - numberFormat.format(constant.integer.toLong()) - } else constant.toPlainText() + CONSTANT -> when (constant.realTypeCase) { + IRRATIONAL -> numberFormat.format(constant.irrational) + INTEGER -> numberFormat.format(constant.integer.toLong()) + // Note that rational types should not actually be encountered in raw expressions, so + // there's no explicit support for reading them out. + RATIONAL, REALTYPE_NOT_SET, null -> null + } VARIABLE -> when (variable) { "z" -> "zed" "Z" -> "Zed" @@ -122,20 +106,13 @@ class MathExpressionAccessibilityUtil @Inject constructor() { "$lhsStr $rhsStr" } else "$lhsStr times $rhsStr" } - DIVIDE -> { - if (divAsFraction && lhs.isConstantInteger() && rhs.isConstantInteger()) { - val numerator = lhs.constant.integer - val denominator = rhs.constant.integer - if (numerator in 0..10 && denominator in 1..10 && denominator >= numerator) { - val ordinalName = - if (numerator == 1) { - singularOrdinalNames.getValue(denominator) - } else pluralOrdinalNames.getValue(denominator) - "$numerator $ordinalName" - } else "$lhsStr over $rhsStr" - } else if (divAsFraction) { - "the fraction with numerator $lhsStr and denominator $rhsStr" - } else "$lhsStr divided by $rhsStr" + DIVIDE -> when { + divAsFraction -> when { + binaryOperation.isOneHalf() -> "one half" + binaryOperation.isSimpleFraction() -> "$lhsStr over $rhsStr" + else -> "the fraction with numerator $lhsStr and denominator $rhsStr" + } + else -> "$lhsStr divided by $rhsStr" } EXPONENTIATE -> "$lhsStr raised to the power of $rhsStr" BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> null @@ -176,8 +153,16 @@ class MathExpressionAccessibilityUtil @Inject constructor() { return rightOperand.isVariable() || rightOperand.isExponentiation() } - private fun MathExpression.isConstantInteger(): Boolean = - expressionTypeCase == CONSTANT && constant.realTypeCase == INTEGER + private fun MathBinaryOperation.isSimpleFraction(): Boolean { + // 'Simple' fractions are those with single term numerators and denominators (which are + // subsequently easier to read out), and whose constant numerator/denonominators are integers. + return leftOperand.isSimpleFractionTerm() && rightOperand.isSimpleFractionTerm() + } + + private fun MathBinaryOperation.isOneHalf(): Boolean { + // If the either operand isn't an integer it will default to 0 per proto3 rules. + return leftOperand.constant.integer == 1 && rightOperand.constant.integer == 2 + } private fun MathExpression.isConstant(): Boolean = expressionTypeCase == CONSTANT @@ -192,5 +177,11 @@ class MathExpressionAccessibilityUtil @Inject constructor() { GROUP -> group.isSingleTerm() EXPRESSIONTYPE_NOT_SET, null -> false } + + private fun MathExpression.isSimpleFractionTerm(): Boolean = when (expressionTypeCase) { + CONSTANT -> constant.realTypeCase == INTEGER + VARIABLE -> true + BINARY_OPERATION, UNARY_OPERATION, FUNCTION_CALL, GROUP, EXPRESSIONTYPE_NOT_SET, null -> false + } } } diff --git a/app/src/test/java/org/oppia/android/app/utility/math/BUILD.bazel b/app/src/test/java/org/oppia/android/app/utility/math/BUILD.bazel index efbfba1af52..48908b50a8c 100644 --- a/app/src/test/java/org/oppia/android/app/utility/math/BUILD.bazel +++ b/app/src/test/java/org/oppia/android/app/utility/math/BUILD.bazel @@ -16,9 +16,11 @@ oppia_android_test( "//app/src/main/java/org/oppia/android/app/utility/math:math_expression_accessibility_util", "//model/src/main/proto:languages_java_proto_lite", "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", - "//third_party:androidx_test_ext_junit", + "//third_party:androidx_test_core", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", diff --git a/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt b/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt index c536a5b8c03..baa7ea46a6e 100644 --- a/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt +++ b/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.app.utility.math import android.app.Application import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.StringSubject import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage @@ -13,8 +12,11 @@ import javax.inject.Singleton import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.oppia.android.app.model.MathBinaryOperation import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathFunctionCall +import org.oppia.android.app.model.MathUnaryOperation import org.oppia.android.app.model.OppiaLanguage import org.oppia.android.app.model.OppiaLanguage.ARABIC import org.oppia.android.app.model.OppiaLanguage.BRAZILIAN_PORTUGUESE @@ -24,854 +26,1160 @@ import org.oppia.android.app.model.OppiaLanguage.HINGLISH import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED +import org.oppia.android.app.model.Real +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner import org.oppia.android.testing.math.MathEquationSubject import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat import org.oppia.android.testing.math.MathExpressionSubject import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.REQUIRED_ONLY import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.ONE_HALF import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode -/** Tests for [MathExpressionAccessibilityUtil]. */ -@RunWith(AndroidJUnit4::class) +/** + * Tests for [MathExpressionAccessibilityUtil]. + * + * Note that this test suite does not make an effort to differentiate tests for numeric and + * algebraic expressions since it's mainly testing [MathExpression] and [MathEquation] structures, + * and relies on other test suites to verify that raw numeric expressions can be correctly converted + * to [MathExpression]s. + */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = MathExpressionAccessibilityUtilTest.TestApplication::class) class MathExpressionAccessibilityUtilTest { - @Inject - lateinit var util: MathExpressionAccessibilityUtil + @Inject lateinit var util: MathExpressionAccessibilityUtil - // TODO: finish tests - - @Before - fun setUp() { - setUpTestApplicationComponent() - } - - @Test - fun test1() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") - assertThat(exp).forHumanReadable(ARABIC).doesNotConvertToString() - } - - @Test - fun test2() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") - assertThat(exp).forHumanReadable(HINDI).doesNotConvertToString() - } - - @Test - fun test3() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") - assertThat(exp).forHumanReadable(HINGLISH).doesNotConvertToString() - } - - @Test - fun test4() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") - assertThat(exp).forHumanReadable(PORTUGUESE).doesNotConvertToString() - } + @Parameter lateinit var language: String + @Parameter lateinit var expression: String + @Parameter lateinit var equation: String + @Parameter lateinit var a11yStr: String @Test - fun test5() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") - assertThat(exp).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() - } + fun testConvertToString_defaultExp_english_returnsNull() { + val exp = MathExpression.getDefaultInstance() - @Test - fun test6() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") - assertThat(exp).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + assertThat(exp).forHumanReadable(ENGLISH).doesNotConvertToString() } @Test - fun test7() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") - assertThat(exp).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() - } + fun testConvertToString_defaultEq_english_returnsNull() { + val eq = MathEquation.getDefaultInstance() - @Test - fun test8() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") - assertThat(exp).forHumanReadable(ARABIC).doesNotConvertToString() + assertThat(eq).forHumanReadable(ENGLISH).doesNotConvertToString() } @Test - fun test9() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") - assertThat(exp).forHumanReadable(HINDI).doesNotConvertToString() - } - - @Test - fun test10() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") - assertThat(exp).forHumanReadable(HINGLISH).doesNotConvertToString() - } - - @Test - fun test11() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") - assertThat(exp).forHumanReadable(PORTUGUESE).doesNotConvertToString() - } + @RunParameterized( + Iteration("LANGUAGE_UNSPECIFIED", "language=LANGUAGE_UNSPECIFIED"), + Iteration("ARABIC", "language=ARABIC"), + Iteration("HINDI", "language=HINDI"), + Iteration("HINGLISH", "language=HINGLISH"), + Iteration("PORTUGUESE", "language=PORTUGUESE"), + Iteration("BRAZILIAN_PORTUGUESE", "language=BRAZILIAN_PORTUGUESE"), + Iteration("UNRECOGNIZED", "language=UNRECOGNIZED") + ) + fun testConvertToString_constExp_unsupportedLanguage_returnsNull() { + val exp = parseAlgebraicExpression("2") + val language = OppiaLanguage.valueOf(language) - @Test - fun test12() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") - assertThat(exp).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + assertThat(exp).forHumanReadable(language).doesNotConvertToString() } @Test - fun test13() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") - assertThat(exp).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() - } + @RunParameterized( + Iteration("LANGUAGE_UNSPECIFIED", "language=LANGUAGE_UNSPECIFIED"), + Iteration("ARABIC", "language=ARABIC"), + Iteration("HINDI", "language=HINDI"), + Iteration("HINGLISH", "language=HINGLISH"), + Iteration("PORTUGUESE", "language=PORTUGUESE"), + Iteration("BRAZILIAN_PORTUGUESE", "language=BRAZILIAN_PORTUGUESE"), + Iteration("UNRECOGNIZED", "language=UNRECOGNIZED") + ) + fun testConvertToString_constEq_unsupportedLanguage_returnsNull() { + val eq = parseAlgebraicEquation("x=2") + val language = OppiaLanguage.valueOf(language) - @Test - fun test14() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") - assertThat(exp).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + assertThat(eq).forHumanReadable(language).doesNotConvertToString() } @Test - fun test15() { - // TODO: do something with this test. - val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") - assertThat(eq).forHumanReadable(ARABIC).doesNotConvertToString() + fun testTestSuite_verifyLanguageCoverage_allLanguagesCovered() { + // NOTE TO DEVELOPERS: This is a meta test to verify that the tests above are covering all + // supported languages. If this test ever fails, please make sure to update both the list below + // and other relevant tests in this suite. + assertThat(OppiaLanguage.values()) + .asList() + .containsExactly( + LANGUAGE_UNSPECIFIED, ENGLISH, ARABIC, HINDI, HINGLISH, PORTUGUESE, BRAZILIAN_PORTUGUESE, + UNRECOGNIZED + ) } @Test - fun test16() { - // TODO: do something with this test. - val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") - assertThat(eq).forHumanReadable(HINDI).doesNotConvertToString() - } + @RunParameterized( + Iteration("2", "expression=2", "a11yStr=2"), + Iteration("123", "expression=123", "a11yStr=123"), + Iteration("1234", "expression=1234", "a11yStr=1,234"), + Iteration("12345", "expression=12345", "a11yStr=12,345"), + Iteration("123456", "expression=123456", "a11yStr=123,456"), + Iteration("1234567", "expression=1234567", "a11yStr=1,234,567") + ) + fun testConvertToString_eng_constIntExp_returnsIntegerConvertedString() { + val exp = parseAlgebraicExpression(expression) + + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) + } + + @Test + @RunParameterized( + // Note that some rounding occurs when formatting doubles with decimals. + Iteration("2.0", "expression=2.0", "a11yStr=2"), + Iteration("3.14", "expression=3.14", "a11yStr=3.14"), + Iteration( + "long_pi", "expression=3.14159265358979323846264338327950288419716939937510", "a11yStr=3.142" + ), + Iteration("1234.0", "expression=1234.0", "a11yStr=1,234"), + Iteration("12345.0", "expression=12345.0", "a11yStr=12,345"), + Iteration("123456.0", "expression=123456.0", "a11yStr=123,456"), + Iteration("1234567.0", "expression=1234567.0", "a11yStr=1,234,567"), + Iteration("1234567.987654321", "expression=1234567.987654321", "a11yStr=1,234,567.988"), + // Verify that scientific notation isn't used. + Iteration("small_number", "expression=0.000000000000000000001", "a11yStr=0"), + Iteration( + "large_number", "expression=123456789101112131415.0", "a11yStr=123,456,789,101,112,130,000" + ) + ) + fun testConvertToString_eng_constDoubleExp_returnsDoubleConvertedString() { + val exp = parseAlgebraicExpression(expression) - @Test - fun test17() { - // TODO: do something with this test. - val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") - assertThat(eq).forHumanReadable(HINGLISH).doesNotConvertToString() + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test18() { - // TODO: do something with this test. - val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") - assertThat(eq).forHumanReadable(PORTUGUESE).doesNotConvertToString() - } + @RunParameterized( + Iteration("x", "expression=x", "a11yStr=x"), + Iteration("y", "expression=y", "a11yStr=y"), + Iteration("z", "expression=z", "a11yStr=zed"), + Iteration("X", "expression=X", "a11yStr=X"), + Iteration("Y", "expression=Y", "a11yStr=Y"), + Iteration("Z", "expression=Z", "a11yStr=Zed"), + Iteration("a", "expression=a", "a11yStr=a") + ) + fun testConvertToString_eng_variableExp_returnsVariableNameWithZed() { + val allowedVariables = listOf("a", "x", "y", "z", "X", "Y", "Z") + val exp = parseAlgebraicExpression(expression, allowedVariables) - @Test - fun test19() { - // TODO: do something with this test. - val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") - assertThat(eq).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test20() { - // TODO: do something with this test. - val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") - assertThat(eq).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() - } + @RunParameterized( + Iteration("1+2", "expression=1+2", "a11yStr=1 plus 2"), + Iteration("1+x", "expression=1+x", "a11yStr=1 plus x"), + Iteration("z+1234", "expression=z+1234", "a11yStr=zed plus 1,234"), + Iteration("z+3.14", "expression=z+3.14", "a11yStr=zed plus 3.14"), + Iteration("x+z", "expression=x+z", "a11yStr=x plus zed") + ) + fun testConvertToString_eng_addition_returnsLeftPlusRightString() { + val exp = parseAlgebraicExpression(expression) - @Test - fun test21() { - // TODO: do something with this test. - val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") - assertThat(eq).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test22() { - // TODO: do something with this test. - // specific cases (from rules & other cases): - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") - } + @RunParameterized( + Iteration("1-2", "expression=1-2", "a11yStr=1 minus 2"), + Iteration("1-x", "expression=1-x", "a11yStr=1 minus x"), + Iteration("z-1234", "expression=z-1234", "a11yStr=zed minus 1,234"), + Iteration("z-3.14", "expression=z-3.14", "a11yStr=zed minus 3.14"), + Iteration("x-z", "expression=x-z", "a11yStr=x minus zed") + ) + fun testConvertToString_eng_subtraction_returnsLeftMinusRightString() { + val exp = parseAlgebraicExpression(expression) - @Test - fun test23() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("-1") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("negative 1") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test24() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithoutOptionalErrors("+1") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("positive 1") - } + @RunParameterized( + Iteration("1*2", "expression=1*2", "a11yStr=1 times 2"), + Iteration("1*x", "expression=1*x", "a11yStr=1 times x"), + Iteration("z*1234", "expression=z*1234", "a11yStr=zed times 1,234"), + Iteration("z*3.14", "expression=z*3.14", "a11yStr=zed times 3.14"), + Iteration("x*z", "expression=x*z", "a11yStr=x times zed") + ) + fun testConvertToString_eng_multiplication_returnsLeftTimesRightString() { + val exp = parseAlgebraicExpression(expression) - @Test - fun test25() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithoutOptionalErrors("((1))") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test26() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+2") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 plus 2") - } + @RunParameterized( + Iteration("1/2", "expression=1/2", "a11yStr=1 divided by 2"), + Iteration("1/x", "expression=1/x", "a11yStr=1 divided by x"), + Iteration("z/1234", "expression=z/1234", "a11yStr=zed divided by 1,234"), + Iteration("z/3.14", "expression=z/3.14", "a11yStr=zed divided by 3.14"), + Iteration("x/z", "expression=x/z", "a11yStr=x divided by zed") + ) + fun testConvertToString_eng_division_returnsLeftDividedByRightString() { + val exp = parseAlgebraicExpression(expression) - @Test - fun test27() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1-2") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 minus 2") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test28() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1*2") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 times 2") - } + @RunParameterized( + Iteration("1^2", "expression=1^2", "a11yStr=1 raised to the power of 2"), + Iteration("1^x", "expression=1^x", "a11yStr=1 raised to the power of x"), + Iteration("z^1234", "expression=z^1234", "a11yStr=zed raised to the power of 1,234"), + Iteration("z^3.14", "expression=z^3.14", "a11yStr=zed raised to the power of 3.14"), + Iteration("x^z", "expression=x^z", "a11yStr=x raised to the power of zed") + ) + fun testConvertToString_eng_exponentiation_returnsLeftRaisedToThePowerOfRightString() { + // Some expressions may include variable terms as exponents (which normally isn't allowed). + val exp = parseAlgebraicExpression(expression, errorCheckingMode = REQUIRED_ONLY) - @Test - fun test29() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1/2") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 divided by 2") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test30() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+(1-2)") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("1 plus open parenthesis 1 minus 2 close parenthesis") - } + @RunParameterized( + Iteration("-2", "expression=-2", "a11yStr=negative 2"), + Iteration("-x", "expression=-x", "a11yStr=negative x"), + Iteration("-1234", "expression=-1234", "a11yStr=negative 1,234"), + Iteration("-3.14", "expression=-3.14", "a11yStr=negative 3.14"), + Iteration("-z", "expression=-z", "a11yStr=negative zed") + ) + fun testConvertToString_eng_negation_returnsNegativeOperandString() { + val exp = parseAlgebraicExpression(expression) - @Test - fun test31() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("2^3") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("2 raised to the power of 3") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test32() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("2^(1+2)") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("2 raised to the power of open parenthesis 1 plus 2 close parenthesis") - } + @RunParameterized( + Iteration("+2", "expression=+2", "a11yStr=positive 2"), + Iteration("+x", "expression=+x", "a11yStr=positive x"), + Iteration("+1234", "expression=+1234", "a11yStr=positive 1,234"), + Iteration("+3.14", "expression=+3.14", "a11yStr=positive 3.14"), + Iteration("+z", "expression=+z", "a11yStr=positive zed") + ) + fun testConvertToString_eng_positiveUnary_returnsPositiveOperandString() { + // Allow positive unary operations to verify this case. + val exp = parseAlgebraicExpression(expression, errorCheckingMode = REQUIRED_ONLY) - @Test - fun test33() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("100000*2") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("100,000 times 2") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test34() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("sqrt(2)") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") - } + @RunParameterized( + Iteration("√2", "expression=√2", "a11yStr=square root of 2"), + Iteration("√x", "expression=√x", "a11yStr=square root of x"), + Iteration("√z", "expression=√z", "a11yStr=square root of zed"), + Iteration("√1234", "expression=√1234", "a11yStr=square root of 1,234"), + Iteration("√3.14", "expression=√3.14", "a11yStr=square root of 3.14"), + Iteration("√(2)", "expression=√(2)", "a11yStr=square root of 2"), + Iteration("√(x)", "expression=√(x)", "a11yStr=square root of x"), + Iteration("√(z)", "expression=√(z)", "a11yStr=square root of zed"), + Iteration("√(1234)", "expression=√(1234)", "a11yStr=square root of 1,234"), + Iteration("√(3.14)", "expression=√(3.14)", "a11yStr=square root of 3.14") + ) + fun testConvertToString_eng_inlineSqrt_returnsSquareRootOfArgumentString() { + // Allow for single-term parentheses for testing (even though these cases would normally result + // in errors). + val exp = parseAlgebraicExpression(expression, errorCheckingMode = REQUIRED_ONLY) - @Test - fun test35() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("√2") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test36() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("sqrt(1+2)") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("start square root 1 plus 2 end square root") - } - - @Test - fun test37() { - // TODO: do something with this test. - val singularOrdinalNames = mapOf( - 1 to "oneth", - 2 to "half", - 3 to "third", - 4 to "fourth", - 5 to "fifth", - 6 to "sixth", - 7 to "seventh", - 8 to "eighth", - 9 to "ninth", - 10 to "tenth", + @RunParameterized( + Iteration("sqrt(2)", "expression=sqrt(2)", "a11yStr=square root of 2"), + Iteration("sqrt(x)", "expression=sqrt(x)", "a11yStr=square root of x"), + Iteration("sqrt(z)", "expression=sqrt(z)", "a11yStr=square root of zed"), + Iteration("sqrt(1234)", "expression=sqrt(1234)", "a11yStr=square root of 1,234"), + Iteration("sqrt(3.14)", "expression=sqrt(3.14)", "a11yStr=square root of 3.14") + ) + fun testConvertToString_eng_sqrt_returnsSquareRootOfArgumentString() { + val exp = parseAlgebraicExpression(expression) + + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) + } + + @Test + @RunParameterized( + Iteration("(2)", "expression=(2)", "a11yStr=2"), + Iteration("(x)", "expression=(x)", "a11yStr=x"), + Iteration("(z)", "expression=(z)", "a11yStr=zed"), + Iteration("(1234)", "expression=(1234)", "a11yStr=1,234"), + Iteration("(3.14)", "expression=(3.14)", "a11yStr=3.14"), + Iteration("((2))", "expression=((2))", "a11yStr=2"), + Iteration("((x))", "expression=((x))", "a11yStr=x"), + Iteration("((z))", "expression=((z))", "a11yStr=zed"), + Iteration("((1234))", "expression=((1234))", "a11yStr=1,234"), + Iteration("((3.14))", "expression=((3.14))", "a11yStr=3.14"), + Iteration("(√2)", "expression=(√2)", "a11yStr=square root of 2"), + Iteration("(√x)", "expression=(√x)", "a11yStr=square root of x"), + Iteration("(sqrt(2))", "expression=(sqrt(2))", "a11yStr=square root of 2"), + Iteration("(sqrt(x))", "expression=(sqrt(x))", "a11yStr=square root of x") + ) + fun testConvertToString_eng_group_singleTermOrNestedSingleTerm_returnsDirectString() { + // Allow for single-term parentheses for testing (even though these cases would normally result + // in errors). + val exp = parseAlgebraicExpression(expression, errorCheckingMode = REQUIRED_ONLY) + + // Verify that groups are not included in the final string when they only encapsulate single + // terms. + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) + } + + @Test + @RunParameterized( + Iteration("(1+2)", "expression=(1+2)", "a11yStr=open parenthesis 1 plus 2 close parenthesis"), + Iteration("(1+x)", "expression=(1+x)", "a11yStr=open parenthesis 1 plus x close parenthesis"), + Iteration("(1+z)", "expression=(1+z)", "a11yStr=open parenthesis 1 plus zed close parenthesis"), + Iteration( + "(1+1234)", "expression=(1+1234)", "a11yStr=open parenthesis 1 plus 1,234 close parenthesis" + ), + Iteration( + "(1+3.14)", "expression=(1+3.14)", "a11yStr=open parenthesis 1 plus 3.14 close parenthesis" + ), + Iteration("(1-2)", "expression=(1-2)", "a11yStr=open parenthesis 1 minus 2 close parenthesis"), + Iteration("(x-2)", "expression=(x-2)", "a11yStr=open parenthesis x minus 2 close parenthesis"), + Iteration("(1*2)", "expression=(1*2)", "a11yStr=open parenthesis 1 times 2 close parenthesis"), + Iteration("(x*2)", "expression=(x*2)", "a11yStr=open parenthesis x times 2 close parenthesis"), + Iteration( + "(1/2)", "expression=(1/2)", "a11yStr=open parenthesis 1 divided by 2 close parenthesis" + ), + Iteration( + "(x/2)", "expression=(x/2)", "a11yStr=open parenthesis x divided by 2 close parenthesis" + ), + Iteration( + "(1^2)", + "expression=(1^2)", + "a11yStr=open parenthesis 1 raised to the power of 2 close parenthesis" + ), + Iteration( + "(x^2)", + "expression=(x^2)", + "a11yStr=open parenthesis x raised to the power of 2 close parenthesis" + ), + Iteration("(-2)", "expression=(-2)", "a11yStr=open parenthesis negative 2 close parenthesis"), + Iteration("(-x)", "expression=(-x)", "a11yStr=open parenthesis negative x close parenthesis"), + Iteration("(+2)", "expression=(+2)", "a11yStr=open parenthesis positive 2 close parenthesis"), + Iteration("(+x)", "expression=(+x)", "a11yStr=open parenthesis positive x close parenthesis") + ) + fun testConvertToString_eng_group_nestedOps_returnOpenParensOpCloseParensString() { + // Allow for the outer expression to have redundant parentheses to test cases when groups are + // announced (even though these exact cases would normally result in an error). + val exp = parseAlgebraicExpression(expression, errorCheckingMode = REQUIRED_ONLY) + + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) + } + + @Test + @RunParameterized( + Iteration("√-2", "expression=√-2", "a11yStr=start square root negative 2 end square root"), + Iteration("√-x", "expression=√-x", "a11yStr=start square root negative x end square root"), + Iteration("√+2", "expression=√+2", "a11yStr=start square root positive 2 end square root"), + Iteration("√+x", "expression=√+x", "a11yStr=start square root positive x end square root"), + // Note that these cases compose with the group cases since √ only "attached" to the immediate + // next terms rather than being able to encapsulate a whole operation (like sqrt()). + Iteration( + "√(1+2)", + "expression=√(1+2)", + "a11yStr=start square root open parenthesis 1 plus 2 close parenthesis end square root" + ), + Iteration( + "√(1+x)", + "expression=√(1+x)", + "a11yStr=start square root open parenthesis 1 plus x close parenthesis end square root" + ), + Iteration( + "√(1-2)", + "expression=√(1-2)", + "a11yStr=start square root open parenthesis 1 minus 2 close parenthesis end square root" + ), + Iteration( + "√(1-x)", + "expression=√(1-x)", + "a11yStr=start square root open parenthesis 1 minus x close parenthesis end square root" + ), + Iteration( + "√(1*2)", + "expression=√(1*2)", + "a11yStr=start square root open parenthesis 1 times 2 close parenthesis end square root" + ), + Iteration( + "√(1*x)", + "expression=√(1*x)", + "a11yStr=start square root open parenthesis 1 times x close parenthesis end square root" + ), + Iteration( + "√(1/2)", + "expression=√(1/2)", + "a11yStr=start square root open parenthesis 1 divided by 2 close parenthesis end square root" + ), + Iteration( + "√(1/x)", + "expression=√(1/x)", + "a11yStr=start square root open parenthesis 1 divided by x close parenthesis end square root" + ), + Iteration( + "√(1^2)", + "expression=√(1^2)", + "a11yStr=start square root open parenthesis 1 raised to the power of 2 close parenthesis" + + " end square root" + ), + Iteration( + "√(1^x)", + "expression=√(1^x)", + "a11yStr=start square root open parenthesis 1 raised to the power of x close parenthesis" + + " end square root" + ), + Iteration( + "√(-2)", + "expression=√(-2)", + "a11yStr=start square root open parenthesis negative 2 close parenthesis end square root" + ), + Iteration( + "√(-x)", + "expression=√(-x)", + "a11yStr=start square root open parenthesis negative x close parenthesis end square root" + ), + Iteration( + "√(+2)", + "expression=√(+2)", + "a11yStr=start square root open parenthesis positive 2 close parenthesis end square root" + ), + Iteration( + "√(+x)", + "expression=√(+x)", + "a11yStr=start square root open parenthesis positive x close parenthesis end square root" ) - val pluralOrdinalNames = mapOf( - 1 to "oneths", - 2 to "halves", - 3 to "thirds", - 4 to "fourths", - 5 to "fifths", - 6 to "sixths", - 7 to "sevenths", - 8 to "eighths", - 9 to "ninths", - 10 to "tenths", + ) + fun testConvertToString_eng_inlineSqrt_nestedOp_returnsStartSquareRootConstructString() { + // Allow for positive unary expressions. + val exp = parseAlgebraicExpression(expression, errorCheckingMode = REQUIRED_ONLY) + + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) + } + + @Test + @RunParameterized( + Iteration( + "sqrt(1+2)", "expression=sqrt(1+2)", "a11yStr=start square root 1 plus 2 end square root" + ), + Iteration( + "sqrt(1+x)", "expression=sqrt(1+x)", "a11yStr=start square root 1 plus x end square root" + ), + Iteration( + "sqrt(1-2)", "expression=sqrt(1-2)", "a11yStr=start square root 1 minus 2 end square root" + ), + Iteration( + "sqrt(1-x)", "expression=sqrt(1-x)", "a11yStr=start square root 1 minus x end square root" + ), + Iteration( + "sqrt(1*2)", "expression=sqrt(1*2)", "a11yStr=start square root 1 times 2 end square root" + ), + Iteration( + "sqrt(1*x)", "expression=sqrt(1*x)", "a11yStr=start square root 1 times x end square root" + ), + Iteration( + "sqrt(1/2)", + "expression=sqrt(1/2)", + "a11yStr=start square root 1 divided by 2 end square root" + ), + Iteration( + "sqrt(1/x)", + "expression=sqrt(1/x)", + "a11yStr=start square root 1 divided by x end square root" + ), + Iteration( + "sqrt(1^2)", + "expression=sqrt(1^2)", + "a11yStr=start square root 1 raised to the power of 2 end square root" + ), + Iteration( + "sqrt(1^x)", + "expression=sqrt(1^x)", + "a11yStr=start square root 1 raised to the power of x end square root" + ), + Iteration( + "sqrt(-2)", "expression=sqrt(-2)", "a11yStr=start square root negative 2 end square root" + ), + Iteration( + "sqrt(-x)", "expression=sqrt(-x)", "a11yStr=start square root negative x end square root" + ), + Iteration( + "sqrt(+2)", "expression=sqrt(+2)", "a11yStr=start square root positive 2 end square root" + ), + Iteration( + "sqrt(+x)", "expression=sqrt(+x)", "a11yStr=start square root positive x end square root" ) - for (denominatorToCheck in 1..10) { - for (numeratorToCheck in 0..denominatorToCheck) { - val exp = - parseNumericExpressionSuccessfullyWithAllErrors("$numeratorToCheck/$denominatorToCheck") - - val ordinalName = - if (numeratorToCheck == 1) { - singularOrdinalNames.getValue(denominatorToCheck) - } else pluralOrdinalNames.getValue(denominatorToCheck) - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("$numeratorToCheck $ordinalName") - } - } - } - - @Test - fun test38() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("-1/3") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("negative 1 third") - } - - @Test - fun test39() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("-2/3") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("negative 2 thirds") - } - - @Test - fun test40() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("10/11") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("10 over 11") - } - - @Test - fun test41() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("121/7986") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("121 over 7,986") - } - - @Test - fun test42() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("8/7") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("8 over 7") - } - - @Test - fun test43() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("-10/-30") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("negative the fraction with numerator 10 and denominator negative 30") - } - - @Test - fun test44() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") - } - - @Test - fun test45() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("((1))") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") - } - - @Test - fun test46() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x") - } - - @Test - fun test47() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("((x))") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x") - } - - @Test - fun test48() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("-x") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("negative x") - } - - @Test - fun test49() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("+x") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("positive x") - } + ) + fun testConvertToString_eng_sqrt_nestedOp_returnsStartSquareRootConstructString() { + // Allow for positive unary expressions. + val exp = parseAlgebraicExpression(expression, errorCheckingMode = REQUIRED_ONLY) - @Test - fun test50() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 plus x") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test51() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1-x") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 minus x") - } + @RunParameterized( + // Note that numeric exponentiations must be explicitly multiplied next to a constant. They + // otherwise result in a grammatical error that cannot be resolved. + Iteration("2x", "expression=2x", "a11yStr=2 x"), + Iteration("2z", "expression=2z", "a11yStr=2 zed"), + Iteration("2x^3", "expression=2x^3", "a11yStr=2 x raised to the power of 3"), + Iteration("2z^3", "expression=2z^3", "a11yStr=2 zed raised to the power of 3"), + Iteration("1234x^3.14", "expression=1234x^3.14", "a11yStr=1,234 x raised to the power of 3.14") + ) + fun testConvertToString_eng_implicitMult_leftConst_rightVarOrExp_returnsLeftRightString() { + val exp = parseAlgebraicExpression(expression) + + // Verify that the format [^ ] results in an implicit multiplication with + // no 'times' announced. + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) + } + + @Test + @RunParameterized( + Iteration("xz", "expression=xz", "a11yStr=x times zed"), + Iteration("2xy", "expression=2yx", "a11yStr=2 y times x"), + Iteration("2√x", "expression=2√x", "a11yStr=2 times square root of x"), + Iteration("2sqrt(x)", "expression=2sqrt(x)", "a11yStr=2 times square root of x"), + Iteration("2(3)", "expression=2(3)", "a11yStr=2 times 3"), + Iteration("2(x)", "expression=2(x)", "a11yStr=2 times x"), + Iteration( + "2(x^3)", + "expression=2(x^3)", + "a11yStr=2 times open parenthesis x raised to the power of 3 close parenthesis" + ) + ) + fun testConvertToString_eng_impMult_nonLeftConst_orRightIsNotVarOrExp_returnsLeftTimesRightStr() { + // Allow for redundant single-term parentheses. + val exp = parseAlgebraicExpression(expression, errorCheckingMode = REQUIRED_ONLY) - @Test - fun test52() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1*x") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 times x") + // If anything breaks up the format tested in the previous test (even if it's a group), then the + // multiplication is explicitly read out. + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test53() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1/x") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 divided by x") - } + fun testConvertToString_eng_divisionAsFractions_oneDivTwo_returnsOneHalfString() { + val exp = parseAlgebraicExpression("1/2") - @Test - fun test54() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1/x") + // 1/2 is a special case. assertThat(exp) .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("the fraction with numerator 1 and denominator x") - } + .convertsWithFractionsToStringThat().isEqualTo("one half") + } + + @Test + @RunParameterized( + Iteration("0/1", "expression=0/1", "a11yStr=0 over 1"), + Iteration("1/1", "expression=1/1", "a11yStr=1 over 1"), + Iteration("0/2", "expression=0/2", "a11yStr=0 over 2"), + Iteration("2/2", "expression=2/2", "a11yStr=2 over 2"), + Iteration("0/3", "expression=0/3", "a11yStr=0 over 3"), + Iteration("1/3", "expression=1/3", "a11yStr=1 over 3"), + Iteration("2/3", "expression=2/3", "a11yStr=2 over 3"), + Iteration("3/3", "expression=3/3", "a11yStr=3 over 3"), + Iteration("4/3", "expression=4/3", "a11yStr=4 over 3"), + Iteration("5/3", "expression=5/3", "a11yStr=5 over 3"), + Iteration("6/3", "expression=6/3", "a11yStr=6 over 3"), + Iteration("5/9", "expression=5/9", "a11yStr=5 over 9"), + Iteration("19/3", "expression=19/3", "a11yStr=19 over 3"), + Iteration("2/17", "expression=2/17", "a11yStr=2 over 17") + ) + fun testConvertToString_eng_divisionAsFractions_smallIntegerFracs_returnsNumOverDenomString() { + val exp = parseAlgebraicExpression(expression) - @Test - fun test55() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(1-x)") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("1 plus open parenthesis 1 minus x close parenthesis") + assertThat(exp).forHumanReadable(ENGLISH).convertsWithFractionsToStringThat().isEqualTo(a11yStr) } @Test - fun test56() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2x") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("2 x") - } + @RunParameterized( + Iteration("1/1234", "expression=1/1234", "a11yStr=1 over 1,234"), + Iteration("1234/1", "expression=1234/1", "a11yStr=1,234 over 1"), + Iteration("1234/987654", "expression=1234/987654", "a11yStr=1,234 over 987,654") + ) + fun testConvertToString_eng_divisionAsFractions_largeIntegerFracs_returnsNumOverDenomString() { + val exp = parseAlgebraicExpression(expression) - @Test - fun test57() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x times y") + // Large numbers are read as part of the fraction. + assertThat(exp).forHumanReadable(ENGLISH).convertsWithFractionsToStringThat().isEqualTo(a11yStr) } @Test - fun test58() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("z") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("zed") - } + @RunParameterized( + Iteration("1/x", "expression=1/x", "a11yStr=1 over x"), + Iteration("1/z", "expression=1/z", "a11yStr=1 over zed"), + Iteration("x/2", "expression=x/2", "a11yStr=x over 2"), + Iteration("z/3", "expression=z/3", "a11yStr=zed over 3"), + Iteration("x/z", "expression=x/z", "a11yStr=x over zed") + ) + fun testConvertToString_eng_divisionAsFractions_fracsWithVariables_returnsNumOverDenomString() { + val exp = parseAlgebraicExpression(expression) + + // Variables are read as part of the fraction. + assertThat(exp).forHumanReadable(ENGLISH).convertsWithFractionsToStringThat().isEqualTo(a11yStr) + } + + @Test + @RunParameterized( + Iteration( + "x/√2", + "expression=x/√2", + "a11yStr=the fraction with numerator x and denominator square root of 2" + ), + Iteration( + "x/-2", + "expression=x/-2", + "a11yStr=the fraction with numerator x and denominator negative 2" + ), + Iteration( + "2/(1+2)", + "expression=2/(1+2)", + "a11yStr=the fraction with numerator 2 and denominator open parenthesis 1 plus 2 close" + + " parenthesis" + ), + // Nested fractions still cause the outer fraction to be read out the long way. + Iteration( + "2/(1/2)", + "expression=2/(1/2)", + "a11yStr=the fraction with numerator 2 and denominator open parenthesis one half close" + + " parenthesis" + ), + Iteration( + "2/(1/3)", + "expression=2/(1/3)", + "a11yStr=the fraction with numerator 2 and denominator open parenthesis 1 over 3 close" + + " parenthesis" + ), + Iteration( + "x/sqrt(y/3)", + "expression=x/sqrt(y/3)", + "a11yStr=the fraction with numerator x and denominator start square root y over 3 end" + + " square root" + ), + Iteration( + "3.14/x", "expression=3.14/x", "a11yStr=the fraction with numerator 3.14 and denominator x" + ), + Iteration( + "x/3.14", "expression=x/3.14", "a11yStr=the fraction with numerator x and denominator 3.14" + ) + ) + fun testConvertToString_eng_divisionAsFractions_fracWithComplexParts_returnsFracConstructStr() { + val exp = parseAlgebraicExpression(expression) - @Test - fun test59() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2xz") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("2 x times zed") + // Verify that complex fractions are read out with more specificity. + assertThat(exp).forHumanReadable(ENGLISH).convertsWithFractionsToStringThat().isEqualTo(a11yStr) } @Test - fun test60() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("x raised to the power of 2") - } + @RunParameterized( + Iteration("1=2", "expression=1=2", "a11yStr=1 equals 2"), + Iteration("x=1", "expression=x=1", "a11yStr=x equals 1"), + Iteration("z=1", "expression=z=1", "a11yStr=zed equals 1"), + Iteration("2=x", "expression=2=x", "a11yStr=2 equals x"), + Iteration("2=z", "expression=2=z", "a11yStr=2 equals zed"), + Iteration("x=z", "expression=x=z", "a11yStr=x equals zed") + ) + fun testConvertToString_eng_simpleEquation_returnsLeftEqualsRightString() { + val eq = parseAlgebraicEquation(expression) - @Test - fun test61() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("x^(1+x)") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("x raised to the power of open parenthesis 1 plus x close parenthesis") + assertThat(eq).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test62() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("100000*2") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("100,000 times 2") - } + @RunParameterized( + Iteration("xyz", "expression=xyz", "a11yStr=x times y times zed"), + Iteration("1+x+x^2", "expression=1+x+x^2", "a11yStr=1 plus x plus x raised to the power of 2"), + Iteration( + "-3x^2+23x-14", + "expression=-3x^2+23x-14", + "a11yStr=negative 3 times x raised to the power of 2 plus 23 x minus 14" + ), + Iteration( + "y^2+xy+x^2", + "expression=y^2+xy+x^2", + "a11yStr=y raised to the power of 2 plus x times y plus x raised to the power of 2" + ) + ) + fun testConvertToString_eng_polynomialExpressions_returnsCorrectlyBuiltString() { + val exp = parseAlgebraicExpression(expression) + + // Polynomials should be read out correctly. + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) + } + + @Test + @RunParameterized( + Iteration("z=xyz", "expression=z=xyz", "a11yStr=zed equals x times y times zed"), + Iteration( + "y=1+x+x^2", + "expression=y=1+x+x^2", + "a11yStr=y equals 1 plus x plus x raised to the power of 2" + ), + Iteration( + "-3x^2+23x-14=7y^3", + "expression=-3x^2+23x-14=7y^3", + "a11yStr=negative 3 times x raised to the power of 2 plus 23 x minus 14 equals 7 y raised" + + " to the power of 3" + ), + Iteration( + "sqrt(z)=y^2+xy+x^2", + "expression=sqrt(z)=y^2+xy+x^2", + "a11yStr=square root of zed equals y raised to the power of 2 plus x times y plus x raised" + + " to the power of 2" + ) + ) + fun testConvertToString_eng_polynomialEquations_returnsCorrectlyBuiltString() { + val eq = parseAlgebraicEquation(expression) + + // Polynomial equations should be read out correctly. + assertThat(eq).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) + } + + @Test + @RunParameterized( + Iteration( + "(x^2+2x+1)/(x+1)", + "expression= ( x^2 + 2x + 1 ) /( x + 1)", + "a11yStr=open parenthesis x raised to the power of 2 plus 2 x plus 1 close parenthesis" + + " divided by open parenthesis x plus 1 close parenthesis" + ), + Iteration( + "(1/2)x", + "expression=(1/2) x", + "a11yStr=open parenthesis 1 divided by 2 close parenthesis times x" + ), + Iteration( + "(-27x^3)^(1/3)", + "expression=(\t-27x\n^3\r)^(1 / 3) ", + "a11yStr=open parenthesis negative 27 times x raised to the power of 3 close parenthesis" + + " raised to the power of open parenthesis 1 divided by 3 close parenthesis" + ), + Iteration( + "(4x^2)^(-1/2)", + "expression=( 4x ^ 2) ^ ( - 1 / 2 ) ", + "a11yStr=open parenthesis 4 x raised to the power of 2 close parenthesis raised to the" + + " power of open parenthesis negative 1 divided by 2 close parenthesis" + ), + Iteration( + "sqrt(sqrt(sqrt(x)+1))", + "expression=sqrt( sqrt( sqrt( x ) + 1 ) )", + "a11yStr=square root of start square root square root of x plus 1 end square root" + ), + Iteration( + "x-(1+(y-(2+z)))", + "expression= x - ( 1 + ( y - ( 2 + z )))", + "a11yStr=x minus open parenthesis 1 plus open parenthesis y minus open parenthesis 2 plus" + + " zed close parenthesis close parenthesis close parenthesis" + ), + Iteration( + "1/(2/(y+3/z))", + "expression=1 / ( 2 / ( y + 3/z ) )", + "a11yStr=1 divided by open parenthesis 2 divided by open parenthesis y plus 3 divided by" + + " zed close parenthesis close parenthesis" + ), + Iteration( + "x/y/z/2", "expression= x/ y/ z/ 2", "a11yStr=x divided by y divided by zed divided by 2" + ) + ) + fun testConvertToString_eng_complexNestedExpression_returnsCorrectlyBuiltString() { + val exp = parseAlgebraicExpression(expression) + + // The expression should correctly convert to a readable string, and all original whitespace + // should be ignored in the final rendered string. + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) + } + + @Test + @RunParameterized( + Iteration( + "(x^2+2x+1)/(x+1)", + "expression= ( x^2 + 2x + 1 ) /( x + 1)", + "a11yStr=the fraction with numerator open parenthesis x raised to the power of 2 plus 2 x" + + " plus 1 close parenthesis and denominator open parenthesis x plus 1 close parenthesis" + ), + Iteration( + "(1/2)x", "expression=(1/2) x", "a11yStr=open parenthesis one half close parenthesis times x" + ), + Iteration( + "(-27x^3)^(1/3)", + "expression=(\t-27x\n^3\r)^(1 / 3) ", + "a11yStr=open parenthesis negative 27 times x raised to the power of 3 close parenthesis" + + " raised to the power of open parenthesis 1 over 3 close parenthesis" + ), + Iteration( + "(4x^2)^(-1/2)", + "expression=( 4x ^ 2) ^ ( - 1 / 2 ) ", + "a11yStr=open parenthesis 4 x raised to the power of 2 close parenthesis raised to the" + + " power of open parenthesis the fraction with numerator negative 1 and denominator 2" + + " close parenthesis" + ), + Iteration( + "sqrt(sqrt(sqrt(x)+1))", + "expression=sqrt( sqrt( sqrt( x ) + 1 ) )", + "a11yStr=square root of start square root square root of x plus 1 end square root" + ), + Iteration( + "x-(1+(y-(2+z)))", + "expression= x - ( 1 + ( y - ( 2 + z )))", + "a11yStr=x minus open parenthesis 1 plus open parenthesis y minus open parenthesis 2 plus" + + " zed close parenthesis close parenthesis close parenthesis" + ), + Iteration( + "1/(2/(y+3/z))", + "expression=1 / ( 2 / ( y + 3/z ) )", + "a11yStr=the fraction with numerator 1 and denominator open parenthesis the fraction with" + + " numerator 2 and denominator open parenthesis y plus 3 over zed close parenthesis" + + " close parenthesis" + ), + Iteration( + "x/y/z/2", + "expression= x/ y/ z/ 2", + "a11yStr=the fraction with numerator the fraction with numerator x over y and denominator" + + " zed and denominator 2" + ) + ) + fun testConvertToString_eng_complexNestedExpression_divAsFracs_returnsCorrectlyBuiltString() { + val exp = parseAlgebraicExpression(expression) + + // The expression should correctly convert to a readable string, and all original whitespace + // should be ignored in the final rendered string. + assertThat(exp).forHumanReadable(ENGLISH).convertsWithFractionsToStringThat().isEqualTo(a11yStr) + } + + @Test + @RunParameterized( + Iteration( + "y=(x^2+2x+1)/(x+1)", + "expression= y = ( x^2 + 2x + 1 ) /( x + 1)", + "a11yStr=y equals open parenthesis x raised to the power of 2 plus 2 x plus 1 close" + + " parenthesis divided by open parenthesis x plus 1 close parenthesis" + ), + Iteration( + "(1/2)x=sqrt(x)", + "expression=(1/2) x =sqrt (x)", + "a11yStr=open parenthesis 1 divided by 2 close parenthesis times x equals square root of x" + ), + Iteration( + "-3x=(-27x^3)^(1/3)", + "expression=\n-\n3\nx\n=\n(\t-27x\n^3\r)^(1 / 3) ", + "a11yStr=negative 3 times x equals open parenthesis negative 27 times x raised to the power" + + " of 3 close parenthesis raised to the power of open parenthesis 1 divided by 3 close" + + " parenthesis" + ), + Iteration( + "(4x^2)^(-1/2)=1+x", + "expression=( 4x ^ 2) ^ ( - 1 / 2 ) =1 + x ", + "a11yStr=open parenthesis 4 x raised to the power of 2 close parenthesis raised to the" + + " power of open parenthesis negative 1 divided by 2 close parenthesis equals 1 plus x" + ), + Iteration( + "sqrt(sqrt(sqrt(x)+1))=1/2", + "expression=sqrt( sqrt( sqrt( x ) + 1 ) ) = 1 / 2", + "a11yStr=square root of start square root square root of x plus 1 end square root equals 1" + + " divided by 2" + ), + Iteration( + "xy+x+y=x-(1+(y-(2+z)))", + "expression=xy+x+y=x - ( 1 + ( y - ( 2 + z )))", + "a11yStr=x times y plus x plus y equals x minus open parenthesis 1 plus open parenthesis y" + + " minus open parenthesis 2 plus zed close parenthesis close parenthesis close parenthesis" + ), + Iteration( + "x=1/(2/(y+3/z))", + "expression= x = 1 / ( 2 / ( y + 3/z ) )", + "a11yStr=x equals 1 divided by open parenthesis 2 divided by open parenthesis y plus 3" + + " divided by zed close parenthesis close parenthesis" + ), + Iteration( + "x/y/z/2=z", + "expression= x/ y/ z/ 2=z", + "a11yStr=x divided by y divided by zed divided by 2 equals zed" + ) + ) + fun testConvertToString_eng_complexNestedEquations_returnsCorrectlyBuiltString() { + val eq = parseAlgebraicEquation(expression) + + // The equation should correctly convert to a readable string, and all original whitespace + // should be ignored in the final rendered string. + assertThat(eq).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) + } + + @Test + @RunParameterized( + Iteration( + "y=(x^2+2x+1)/(x+1)", + "expression= y = ( x^2 + 2x + 1 ) /( x + 1)", + "a11yStr=y equals the fraction with numerator open parenthesis x raised to the power of 2" + + " plus 2 x plus 1 close parenthesis and denominator open parenthesis x plus 1 close" + + " parenthesis" + ), + Iteration( + "(1/2)x=sqrt(x)", + "expression=(1/2) x =sqrt (x)", + "a11yStr=open parenthesis one half close parenthesis times x equals square root of x" + ), + Iteration( + "-3x=(-27x^3)^(1/3)", + "expression=\n-\n3\nx\n=\n(\t-27x\n^3\r)^(1 / 3) ", + "a11yStr=negative 3 times x equals open parenthesis negative 27 times x raised to the power" + + " of 3 close parenthesis raised to the power of open parenthesis 1 over 3 close parenthesis" + ), + Iteration( + "(4x^2)^(-1/2)=1+x", + "expression=( 4x ^ 2) ^ ( - 1 / 2 ) =1 + x ", + "a11yStr=open parenthesis 4 x raised to the power of 2 close parenthesis raised to the" + + " power of open parenthesis the fraction with numerator negative 1 and denominator 2" + + " close parenthesis equals 1 plus x" + ), + Iteration( + "sqrt(sqrt(sqrt(x)+1))=1/2", + "expression=sqrt( sqrt( sqrt( x ) + 1 ) ) = 1 / 2", + "a11yStr=square root of start square root square root of x plus 1 end square root equals" + + " one half" + ), + Iteration( + "xy+x+y=x-(1+(y-(2+z)))", + "expression=xy+x+y=x - ( 1 + ( y - ( 2 + z )))", + "a11yStr=x times y plus x plus y equals x minus open parenthesis 1 plus open parenthesis y" + + " minus open parenthesis 2 plus zed close parenthesis close parenthesis close parenthesis" + ), + Iteration( + "x=1/(2/(y+3/z))", + "expression= x = 1 / ( 2 / ( y + 3/z ) )", + "a11yStr=x equals the fraction with numerator 1 and denominator open parenthesis the" + + " fraction with numerator 2 and denominator open parenthesis y plus 3 over zed close" + + " parenthesis close parenthesis" + ), + Iteration( + "x/y/z/2=z", + "expression= x/ y/ z/ 2=z", + "a11yStr=the fraction with numerator the fraction with numerator x over y and denominator" + + " zed and denominator 2 equals zed" + ) + ) + fun testConvertToString_eng_complexNestedEquations_divAsFracs_returnsCorrectlyBuiltString() { + val eq = parseAlgebraicEquation(expression) + + // The equation should correctly convert to a readable string, and all original whitespace + // should be ignored in the final rendered string. + assertThat(eq).forHumanReadable(ENGLISH).convertsWithFractionsToStringThat().isEqualTo(a11yStr) + } + + // This & the next test are implementing cases defined in the doc: + // https://docs.google.com/document/d/1P-dldXQ08O-02ZRG978paiWOSz0dsvcKpDgiV_rKH_Y/edit#. + @Test + @RunParameterized( + Iteration( + "(x + 6)/(x - 4)", + "expression=(x + 6)/(x - 4)", + "a11yStr=the fraction with numerator open parenthesis x plus 6 close parenthesis and" + + " denominator open parenthesis x minus 4 close parenthesis" + ), + Iteration( + "4*(x)^(2)+20x", + "expression=4*(x)^(2)+20x", + "a11yStr=4 times x raised to the power of 2 plus 20 x" + ), + Iteration("3+x-5", "expression=3+x-5", "a11yStr=3 plus x minus 5"), + Iteration("Z+A-Z", "expression=Z+A-Z", "a11yStr=Zed plus A minus Zed"), + Iteration("6C - 5A -1", "expression=6C - 5A -1", "a11yStr=6 C minus 5 A minus 1"), + Iteration("5*Z-w", "expression=5*Z-w", "a11yStr=5 times Zed minus w"), + Iteration("L*S-3S+L", "expression=L*S-3S+L", "a11yStr=L times S minus 3 S plus L"), + Iteration( + "2*(2+6+3+4)", + "expression=2*(2+6+3+4)", + "a11yStr=2 times open parenthesis 2 plus 6 plus 3 plus 4 close parenthesis" + ), + Iteration("sqrt(64)", "expression=sqrt(64)", "a11yStr=square root of 64"), + Iteration( + "√(a+b)", + "expression=√(a+b)", + "a11yStr=start square root open parenthesis a plus b close parenthesis end square root" + ), + Iteration( + "3 * 10^-5", "expression=3 * 10^-5", "a11yStr=3 times 10 raised to the power of negative 5" + ), + Iteration( + "((x+2y) + 5*(a - 2b) + z)", + "expression=((x+2y) + 5*(a - 2b) + z)", + "a11yStr=open parenthesis open parenthesis x plus 2 y close parenthesis plus 5 times open" + + " parenthesis a minus 2 b close parenthesis plus zed close parenthesis" + ) + ) + fun testConvertToString_eng_assortedExpressionsFromPrd_returnsCorrectlyComputedString() { + // Some of the expressions include cases that would normally result in errors. + val exp = parseAlgebraicExpression(expression, errorCheckingMode = REQUIRED_ONLY) - @Test - fun test63() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(2)") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + assertThat(exp).forHumanReadable(ENGLISH).convertsWithFractionsToStringThat().isEqualTo(a11yStr) } @Test - fun test64() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(x)") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of x") - } + @RunParameterized( + Iteration( + "3x^2 + 4y = 62", + "expression=3x^2 + 4y = 62", + "a11yStr=3 x raised to the power of 2 plus 4 y equals 62" + ) + ) + fun testConvertToString_eng_assortedEquationsFromPrd_returnsCorrectlyComputedString() { + val eq = parseAlgebraicEquation(expression) - @Test - fun test65() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("√2") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + assertThat(eq).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test66() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("√x") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of x") - } + fun testConvertToString_eng_rationalConstant_returnsNull() { + val exp = MathExpression.newBuilder().apply { + constant = ONE_HALF + }.build() - @Test - fun test67() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(1+2)") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("start square root 1 plus 2 end square root") + // The conversion should fail since the expression includes a rational real (which aren't yet + // supported). + assertThat(exp).forHumanReadable(ENGLISH).doesNotConvertToString() } @Test - fun test68() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(1+x)") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("start square root 1 plus x end square root") - } + fun testConvertToString_eng_invalidConstant_returnsNull() { + val exp = MathExpression.newBuilder().apply { + constant = Real.getDefaultInstance() + }.build() - @Test - fun test69() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("√(1+x)") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("start square root open parenthesis 1 plus x close parenthesis end square root") - } - - @Test - fun test70() { - // TODO: do something with this test. - val singularOrdinalNames = mapOf( - 1 to "oneth", - 2 to "half", - 3 to "third", - 4 to "fourth", - 5 to "fifth", - 6 to "sixth", - 7 to "seventh", - 8 to "eighth", - 9 to "ninth", - 10 to "tenth", - ) - val pluralOrdinalNames = mapOf( - 1 to "oneths", - 2 to "halves", - 3 to "thirds", - 4 to "fourths", - 5 to "fifths", - 6 to "sixths", - 7 to "sevenths", - 8 to "eighths", - 9 to "ninths", - 10 to "tenths", - ) - for (denominatorToCheck in 1..10) { - for (numeratorToCheck in 0..denominatorToCheck) { - val exp = - parseAlgebraicExpressionSuccessfullyWithAllErrors("$numeratorToCheck/$denominatorToCheck") - - val ordinalName = - if (numeratorToCheck == 1) { - singularOrdinalNames.getValue(denominatorToCheck) - } else pluralOrdinalNames.getValue(denominatorToCheck) - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("$numeratorToCheck $ordinalName") - } - } + // The conversion should fail since the expression includes an invalid real constant. + assertThat(exp).forHumanReadable(ENGLISH).doesNotConvertToString() } @Test - fun test71() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") - } + fun testConvertToString_eng_invalidBinaryOp_returnsNull() { + val exp = MathExpression.newBuilder().apply { + binaryOperation = MathBinaryOperation.getDefaultInstance() + }.build() - @Test - fun test72() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x(5-y)") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("x times open parenthesis 5 minus y close parenthesis") + // The conversion should fail since the expression includes an invalid binary operation. + assertThat(exp).forHumanReadable(ENGLISH).doesNotConvertToString() } @Test - fun test73() { - // TODO: do something with this test. - val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/y") - assertThat(eq) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("x equals 1 divided by y") - } + fun testConvertToString_eng_invalidUnaryOp_returnsNull() { + val exp = MathExpression.newBuilder().apply { + unaryOperation = MathUnaryOperation.getDefaultInstance() + }.build() - @Test - fun test74() { - // TODO: do something with this test. - val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/2") - assertThat(eq) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("x equals 1 divided by 2") + // The conversion should fail since the expression includes an invalid unary operation. + assertThat(exp).forHumanReadable(ENGLISH).doesNotConvertToString() } @Test - fun test75() { - // TODO: do something with this test. - val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/y") - assertThat(eq) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("x equals the fraction with numerator 1 and denominator y") - } + fun testConvertToString_eng_invalidFunctionType_returnsNull() { + val exp = MathExpression.newBuilder().apply { + functionCall = MathFunctionCall.getDefaultInstance() + }.build() - @Test - fun test76() { - // TODO: do something with this test. - val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/2") - assertThat(eq) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("x equals 1 half") + // The conversion should fail since the expression includes an invalid function call. + assertThat(exp).forHumanReadable(ENGLISH).doesNotConvertToString() } @Test - fun test77() { - // TODO: do something with this test. - // Tests from examples in the PRD - val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("3x^2+4y=62") - assertThat(eq) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("3 x raised to the power of 2 plus 4 y equals 62") - } + fun testConvertToString_eng_nestedDefaultExp_returnsNull() { + val exp = MathExpression.newBuilder().apply { + group = MathExpression.getDefaultInstance() + }.build() - @Test - fun test78() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x+6)/(x-4)") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo( - "the fraction with numerator open parenthesis x plus 6 close parenthesis and denominator" + - " open parenthesis x minus 4 close parenthesis" - ) + // The conversion should fail since the expression includes an invalid nested expression. + assertThat(exp).forHumanReadable(ENGLISH).doesNotConvertToString() } @Test - fun test79() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("4*(x)^(2)+20x") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("4 times x raised to the power of 2 plus 20 x") - } + fun testConvertToString_eng_nestedInvalidBinaryOp_returnsNull() { + val exp = MathExpression.newBuilder().apply { + group = MathExpression.newBuilder().apply { + binaryOperation = MathBinaryOperation.getDefaultInstance() + }.build() + }.build() - @Test - fun test80() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("3+x-5") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("3 plus x minus 5") + // The conversion should fail since the expression includes an invalid nested expression. + assertThat(exp).forHumanReadable(ENGLISH).doesNotConvertToString() } @Test - fun test81() { - // TODO: do something with this test. - val exp = - parseAlgebraicExpressionSuccessfullyWithAllErrors( - "Z+A-Z", allowedVariables = listOf("A", "Z") - ) - assertThat(exp).forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("Zed plus A minus Zed") - } + fun testConvertToString_eng_nestedInvalidUnaryOp_returnsNull() { + val exp = MathExpression.newBuilder().apply { + group = MathExpression.newBuilder().apply { + unaryOperation = MathUnaryOperation.getDefaultInstance() + }.build() + }.build() - @Test - fun test82() { - // TODO: do something with this test. - val exp = - parseAlgebraicExpressionSuccessfullyWithAllErrors( - "6C-5A-1", allowedVariables = listOf("A", "C") - ) - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("6 C minus 5 A minus 1") + // The conversion should fail since the expression includes an invalid nested expression. + assertThat(exp).forHumanReadable(ENGLISH).doesNotConvertToString() } @Test - fun test83() { - // TODO: do something with this test. - val exp = - parseAlgebraicExpressionSuccessfullyWithAllErrors( - "5*Z-w", allowedVariables = listOf("Z", "w") - ) - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("5 times Zed minus w") - } + fun testConvertToString_eng_nestedInvalidFunctionType_returnsNull() { + val exp = MathExpression.newBuilder().apply { + group = MathExpression.newBuilder().apply { + functionCall = MathFunctionCall.getDefaultInstance() + }.build() + }.build() - @Test - fun test84() { - // TODO: do something with this test. - val exp = - parseAlgebraicExpressionSuccessfullyWithAllErrors( - "L*S-3S+L", allowedVariables = listOf("L", "S") - ) - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("L times S minus 3 S plus L") + // The conversion should fail since the expression includes an invalid nested expression. + assertThat(exp).forHumanReadable(ENGLISH).doesNotConvertToString() } @Test - fun test85() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(2+6+3+4)") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("2 times open parenthesis 2 plus 6 plus 3 plus 4 close parenthesis") - } + fun testConvertToString_eq_withLeftInvalidExp_returnsNull() { + val validExp = MathExpression.newBuilder().apply { + constant = ONE_HALF + }.build() + val invalidExp = MathExpression.getDefaultInstance() + val eq = MathEquation.newBuilder().apply { + leftSide = invalidExp + rightSide = validExp + }.build() - @Test - fun test86() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(64)") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("square root of 64") + // Both sides of the equation must be valid. + assertThat(eq).forHumanReadable(ENGLISH).doesNotConvertToString() } @Test - fun test87() { - // TODO: do something with this test. - val exp = - parseAlgebraicExpressionSuccessfullyWithAllErrors( - "√(a+b)", allowedVariables = listOf("a", "b") - ) - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("start square root open parenthesis a plus b close parenthesis end square root") - } + fun testConvertToString_eq_withRightInvalidExp_returnsNull() { + val validExp = MathExpression.newBuilder().apply { + constant = ONE_HALF + }.build() + val invalidExp = MathExpression.getDefaultInstance() + val eq = MathEquation.newBuilder().apply { + leftSide = validExp + rightSide = invalidExp + }.build() - @Test - fun test88() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("3*10^-5") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("3 times 10 raised to the power of negative 5") + // Both sides of the equation must be valid. + assertThat(eq).forHumanReadable(ENGLISH).doesNotConvertToString() } - @Test - fun test89() { - // TODO: do something with this test. - val exp = - parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors( - "((x+2y)+5*(a-2b)+z)", allowedVariables = listOf("x", "y", "a", "b", "z") - ) - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo( - "open parenthesis open parenthesis x plus 2 y close parenthesis plus 5 times open" + - " parenthesis a minus 2 b close parenthesis plus zed close parenthesis" - ) + @Before + fun setUp() { + setUpTestApplicationComponent() } private fun MathExpressionSubject.forHumanReadable( @@ -922,10 +1230,7 @@ class MathExpressionAccessibilityUtilTest { // TODO(#89): Move this to a common test application component. @Singleton - @Component( - modules = [ - ] - ) + @Component interface TestApplicationComponent { @Component.Builder interface Builder { @@ -951,72 +1256,29 @@ class MathExpressionAccessibilityUtilTest { } private companion object { - // TODO: finalize this API. - - private fun parseNumericExpressionSuccessfullyWithAllErrors( - expression: String - ): MathExpression { - val result = parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) - return (result as MathParsingResult.Success).result - } - - private fun parseNumericExpressionSuccessfullyWithoutOptionalErrors( - expression: String - ): MathExpression { - val result = - parseNumericExpressionInternal( - expression, ErrorCheckingMode.REQUIRED_ONLY - ) - return (result as MathParsingResult.Success).result - } - - private fun parseNumericExpressionInternal( - expression: String, - errorCheckingMode: ErrorCheckingMode - ): MathParsingResult { - return MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) - } - - private fun parseAlgebraicExpressionSuccessfullyWithAllErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathExpression { - val result = - parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) - return (result as MathParsingResult.Success).result - } - - private fun parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors( + private fun parseAlgebraicExpression( expression: String, - allowedVariables: List = listOf("x", "y", "z") + allowedVariables: List = listOf("x", "y", "z"), + errorCheckingMode: ErrorCheckingMode = ALL_ERRORS ): MathExpression { - val result = - parseAlgebraicExpressionInternal( - expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables - ) - return (result as MathParsingResult.Success).result - } - - private fun parseAlgebraicExpressionInternal( - expression: String, - errorCheckingMode: ErrorCheckingMode, - allowedVariables: List = listOf("x", "y", "z") - ): MathParsingResult { return MathExpressionParser.parseAlgebraicExpression( expression, allowedVariables, errorCheckingMode - ) + ).getExpectedSuccess() } - private fun parseAlgebraicEquationSuccessfullyWithAllErrors( + private fun parseAlgebraicEquation( expression: String, - allowedVariables: List = listOf("x", "y", "z") ): MathEquation { - val result = - MathExpressionParser.parseAlgebraicEquation( - expression, allowedVariables, - ErrorCheckingMode.ALL_ERRORS - ) - return (result as MathParsingResult.Success).result + return MathExpressionParser.parseAlgebraicEquation( + expression, + allowedVariables = listOf("x", "y", "z"), + errorCheckingMode = ALL_ERRORS + ).getExpectedSuccess() + } + + private inline fun MathParsingResult.getExpectedSuccess(): T { + assertThat(this).isInstanceOf(MathParsingResult.Success::class.java) + return (this as MathParsingResult.Success).result } } } From 4c163bf95605c2ac42c72934d5e15b52e0ce9f2b Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 7 Feb 2022 22:36:01 -0800 Subject: [PATCH 120/134] Clean up + KDocs + exemption. --- .../android/app/testing/activity/BUILD.bazel | 1 + .../app/testing/activity/TestActivity.kt | 4 + .../translation/AppLanguageResourceHandler.kt | 6 + .../android/app/utility/math/BUILD.bazel | 15 +- .../math/MathExpressionAccessibilityUtil.kt | 225 ++++++++++++------ .../main/res/values/untranslated_strings.xml | 17 ++ .../AppLanguageResourceHandlerTest.kt | 33 +++ .../android/app/utility/math/BUILD.bazel | 16 ++ .../MathExpressionAccessibilityUtilTest.kt | 119 ++++++++- .../domain/locale/DisplayLocaleImpl.kt | 6 + .../domain/locale/DisplayLocaleImplTest.kt | 30 +++ .../file_content_validation_checks.textproto | 1 + .../oppia/android/util/locale/OppiaLocale.kt | 19 ++ 13 files changed, 410 insertions(+), 82 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/testing/activity/BUILD.bazel b/app/src/main/java/org/oppia/android/app/testing/activity/BUILD.bazel index 137513d16c5..eb5ef2049c6 100644 --- a/app/src/main/java/org/oppia/android/app/testing/activity/BUILD.bazel +++ b/app/src/main/java/org/oppia/android/app/testing/activity/BUILD.bazel @@ -18,5 +18,6 @@ kt_android_library( deps = [ "//app/src/main/java/org/oppia/android/app/activity:activity_intent_factories_shim", "//app/src/main/java/org/oppia/android/app/activity:injectable_app_compat_activity", + "//app/src/main/java/org/oppia/android/app/utility/math:math_expression_accessibility_util", ], ) diff --git a/app/src/main/java/org/oppia/android/app/testing/activity/TestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/activity/TestActivity.kt index 05307f02df9..72be82faf23 100644 --- a/app/src/main/java/org/oppia/android/app/testing/activity/TestActivity.kt +++ b/app/src/main/java/org/oppia/android/app/testing/activity/TestActivity.kt @@ -8,6 +8,7 @@ import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.translation.AppLanguageWatcherMixin import org.oppia.android.app.utility.datetime.DateTimeUtil import javax.inject.Inject +import org.oppia.android.app.utility.math.MathExpressionAccessibilityUtil // TODO(#3830): Migrate all test activities over to using this test activity & make this closed. /** @@ -36,6 +37,9 @@ open class TestActivity : InjectableAppCompatActivity() { @Inject lateinit var appLanguageWatcherMixin: AppLanguageWatcherMixin + @Inject + lateinit var mathExpressionAccessibilityUtil: MathExpressionAccessibilityUtil + override fun attachBaseContext(newBase: Context?) { super.attachBaseContext(newBase) (activityComponent as Injector).inject(this) diff --git a/app/src/main/java/org/oppia/android/app/translation/AppLanguageResourceHandler.kt b/app/src/main/java/org/oppia/android/app/translation/AppLanguageResourceHandler.kt index 8dd7787dc15..4887f41175a 100644 --- a/app/src/main/java/org/oppia/android/app/translation/AppLanguageResourceHandler.kt +++ b/app/src/main/java/org/oppia/android/app/translation/AppLanguageResourceHandler.kt @@ -115,6 +115,12 @@ class AppLanguageResourceHandler @Inject constructor( } } + /** See [OppiaLocale.DisplayLocale.formatLong] for specific behavior. */ + fun formatLong(value: Long): String = getDisplayLocale().formatLong(value) + + /** See [OppiaLocale.DisplayLocale.formatDouble] for specific behavior. */ + fun formatDouble(value: Double): String = getDisplayLocale().formatDouble(value) + /** See [OppiaLocale.DisplayLocale.computeDateString]. */ fun computeDateString(timestampMillis: Long): String = getDisplayLocale().computeDateString(timestampMillis) diff --git a/app/src/main/java/org/oppia/android/app/utility/math/BUILD.bazel b/app/src/main/java/org/oppia/android/app/utility/math/BUILD.bazel index 5b5ee7cb433..12d6ae08213 100644 --- a/app/src/main/java/org/oppia/android/app/utility/math/BUILD.bazel +++ b/app/src/main/java/org/oppia/android/app/utility/math/BUILD.bazel @@ -5,14 +5,27 @@ General purposes utilities corresponding to displaying math expressions & constr load("@dagger//:workspace_defs.bzl", "dagger_rules") load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") +# Resource shim needed so that MathExpressionAccessibilityUtil can build in both Gradle & Bazel. +genrule( + name = "update_MathExpressionAccessibilityUtil", + srcs = ["MathExpressionAccessibilityUtil.kt"], + outs = ["MathExpressionAccessibilityUtil_updated.kt"], + cmd = """ + cat $(SRCS) | + sed 's/import org.oppia.android.R/import org.oppia.android.app.R/g' > $(OUTS) + """, +) + kt_android_library( name = "math_expression_accessibility_util", srcs = [ - "MathExpressionAccessibilityUtil.kt", + "MathExpressionAccessibilityUtil_updated.kt", ], visibility = ["//app:app_visibility"], deps = [ ":dagger", + "//app:resources", + "//app/src/main/java/org/oppia/android/app/translation:app_language_resource_handler", "//model/src/main/proto:languages_java_proto_lite", "//model/src/main/proto:math_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/math:extensions", diff --git a/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt b/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt index efd658d12b1..5001922b9ab 100644 --- a/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt +++ b/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt @@ -1,6 +1,9 @@ package org.oppia.android.app.utility.math +import org.oppia.android.R +import javax.inject.Inject import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE @@ -18,6 +21,7 @@ import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE import org.oppia.android.app.model.MathFunctionCall.FunctionType import org.oppia.android.app.model.MathFunctionCall.FunctionType.FUNCTION_UNSPECIFIED import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE import org.oppia.android.app.model.OppiaLanguage @@ -30,18 +34,36 @@ import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED import org.oppia.android.app.model.Real.RealTypeCase.INTEGER -import java.text.NumberFormat -import java.util.Locale -import javax.inject.Inject -import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator -import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET +import org.oppia.android.app.translation.AppLanguageResourceHandler -class MathExpressionAccessibilityUtil @Inject constructor() { - // TODO: document that rationals aren't supported, and that irrationals are rounded during formatting (and maybe that ints are also formatted?). - +/** + * Utility for computing an accessibility string for screenreaders to be able to read out parsed + * [MathExpression]s and [MathEquation]s. + * + * See [convertToHumanReadableString] for the specific function. + */ +class MathExpressionAccessibilityUtil @Inject constructor( + private val resourceHandler: AppLanguageResourceHandler +) { + /** + * Returns the human-readable string (for screenreaders) representation of the specified + * [expression]. + * + * Note that rational ``Real``s are specifically not supported and will result in a null value + * being returned (for custom expression constructs should use a division operation and set + * [divAsFraction] to true. Further, irrational reals may be rounded during formatting if they are + * very large or have long decimals (for an easier time reading). Numbers will be formatted + * according to the user's locale. + * + * @param expression the expression to convert + * @param language the target language for which the expression should be generated + * @param divAsFraction whether divisions should be read out as fractions rather than divisions + * @return the human-readable string, or null if the expression is malformed or the target + * language is unsupported + */ fun convertToHumanReadableString( expression: MathExpression, language: OppiaLanguage, @@ -54,6 +76,13 @@ class MathExpressionAccessibilityUtil @Inject constructor() { } } + /** + * Returns the human-readable string (for screenreaders) representation of the specified + * [equation]. + * + * This function behaves in the same way as the [MathExpression] version of + * [convertToHumanReadableString]--see that method's documentation for more details. + */ fun convertToHumanReadableString( equation: MathEquation, language: OppiaLanguage, @@ -66,84 +95,138 @@ class MathExpressionAccessibilityUtil @Inject constructor() { } } - private companion object { - // TODO: move these to the UI layer & have them utilize non-translatable strings. - private val numberFormat by lazy { NumberFormat.getNumberInstance(Locale.US) } + private fun MathEquation.toHumanReadableEnglishString(divAsFraction: Boolean): String? { + val lhsStr = leftSide.toHumanReadableEnglishString(divAsFraction) + val rhsStr = rightSide.toHumanReadableEnglishString(divAsFraction) + return if (lhsStr != null && rhsStr != null) { + resourceHandler.getStringInLocaleWithWrapping( + R.string.math_accessibility_a_equals_b, lhsStr, rhsStr + ) + } else null + } - private fun MathEquation.toHumanReadableEnglishString(divAsFraction: Boolean): String? { - val lhsStr = leftSide.toHumanReadableEnglishString(divAsFraction) - val rhsStr = rightSide.toHumanReadableEnglishString(divAsFraction) - return if (lhsStr != null && rhsStr != null) "$lhsStr equals $rhsStr" else null - } + private fun MathExpression.toHumanReadableEnglishString(divAsFraction: Boolean): String? { + // Reference: + // https://docs.google.com/document/d/1P-dldXQ08O-02ZRG978paiWOSz0dsvcKpDgiV_rKH_Y/view. - private fun MathExpression.toHumanReadableEnglishString(divAsFraction: Boolean): String? { - // Reference: - // https://docs.google.com/document/d/1P-dldXQ08O-02ZRG978paiWOSz0dsvcKpDgiV_rKH_Y/view. - return when (expressionTypeCase) { - CONSTANT -> when (constant.realTypeCase) { - IRRATIONAL -> numberFormat.format(constant.irrational) - INTEGER -> numberFormat.format(constant.integer.toLong()) - // Note that rational types should not actually be encountered in raw expressions, so - // there's no explicit support for reading them out. - RATIONAL, REALTYPE_NOT_SET, null -> null - } - VARIABLE -> when (variable) { - "z" -> "zed" - "Z" -> "Zed" - else -> variable + // Note that extra bidi wrapping is occurring here since there's not an obvious way to wrap "at + // the end" for non-equations. + return when (expressionTypeCase) { + CONSTANT -> when (constant.realTypeCase) { + IRRATIONAL -> resourceHandler.formatDouble(constant.irrational) + INTEGER -> resourceHandler.formatLong(constant.integer.toLong()) + // Note that rational types should not actually be encountered in raw expressions, so + // there's no explicit support for reading them out. + RATIONAL, REALTYPE_NOT_SET, null -> null + } + VARIABLE -> when (variable) { + "z", "Z" -> { + val zed = + resourceHandler.getStringInLocale(R.string.math_accessibility_part_zed) + if (variable == "Z") { + resourceHandler.capitalizeForHumans(zed) + } else zed } - BINARY_OPERATION -> { - val lhs = binaryOperation.leftOperand - val rhs = binaryOperation.rightOperand - val lhsStr = lhs.toHumanReadableEnglishString(divAsFraction) - val rhsStr = rhs.toHumanReadableEnglishString(divAsFraction) - if (lhsStr == null || rhsStr == null) return null - when (binaryOperation.operator) { - ADD -> "$lhsStr plus $rhsStr" - SUBTRACT -> "$lhsStr minus $rhsStr" - MULTIPLY -> { - if (binaryOperation.canBeReadAsImplicitMultiplication()) { - "$lhsStr $rhsStr" - } else "$lhsStr times $rhsStr" - } - DIVIDE -> when { - divAsFraction -> when { - binaryOperation.isOneHalf() -> "one half" - binaryOperation.isSimpleFraction() -> "$lhsStr over $rhsStr" - else -> "the fraction with numerator $lhsStr and denominator $rhsStr" + else -> variable + } + BINARY_OPERATION -> { + val lhs = binaryOperation.leftOperand + val rhs = binaryOperation.rightOperand + val lhsStr = lhs.toHumanReadableEnglishString(divAsFraction) + val rhsStr = rhs.toHumanReadableEnglishString(divAsFraction) + if (lhsStr == null || rhsStr == null) return null + when (binaryOperation.operator) { + ADD -> { + resourceHandler.getStringInLocaleWithWrapping( + R.string.math_accessibility_a_plus_b, lhsStr, rhsStr + ) + } + SUBTRACT -> { + resourceHandler.getStringInLocaleWithWrapping( + R.string.math_accessibility_a_minus_b, lhsStr, rhsStr + ) + } + MULTIPLY -> { + val strResId = if (binaryOperation.canBeReadAsImplicitMultiplication()) { + R.string.math_accessibility_implicit_multiplication + } else R.string.math_accessibility_a_times_b + resourceHandler.getStringInLocaleWithWrapping(strResId, lhsStr, rhsStr) + } + DIVIDE -> when { + divAsFraction -> when { + binaryOperation.isOneHalf() -> { + resourceHandler.getStringInLocaleWithWrapping( + R.string.math_accessibility_part_one_half + ) + } + binaryOperation.isSimpleFraction() -> { + resourceHandler.getStringInLocaleWithWrapping( + R.string.math_accessibility_simple_fraction, lhsStr, rhsStr + ) } - else -> "$lhsStr divided by $rhsStr" + else -> { + resourceHandler.getStringInLocaleWithWrapping( + R.string.math_accessibility_complex_fraction, lhsStr, rhsStr + ) + } + } + else -> { + resourceHandler.getStringInLocaleWithWrapping( + R.string.math_accessibility_a_divides_b, lhsStr, rhsStr + ) } - EXPONENTIATE -> "$lhsStr raised to the power of $rhsStr" - BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> null } + EXPONENTIATE -> { + resourceHandler.getStringInLocaleWithWrapping( + R.string.math_accessibility_a_exp_b, lhsStr, rhsStr + ) + } + BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> null } - UNARY_OPERATION -> { - val operandStr = unaryOperation.operand.toHumanReadableEnglishString(divAsFraction) - when (unaryOperation.operator) { - NEGATE -> operandStr?.let { "negative $it" } - POSITIVE -> operandStr?.let { "positive $it" } - UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> null + } + UNARY_OPERATION -> { + val operandStr = unaryOperation.operand.toHumanReadableEnglishString(divAsFraction) + when (unaryOperation.operator) { + NEGATE -> operandStr?.let { + resourceHandler.getStringInLocaleWithWrapping( + R.string.math_accessibility_negative_a, it + ) + } + POSITIVE -> operandStr?.let { + resourceHandler.getStringInLocaleWithWrapping( + R.string.math_accessibility_positive_a, it + ) } + UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> null } - FUNCTION_CALL -> { - val argStr = functionCall.argument.toHumanReadableEnglishString(divAsFraction) - when (functionCall.functionType) { - SQUARE_ROOT -> argStr?.let { - if (functionCall.argument.isSingleTerm()) { - "square root of $it" - } else "start square root $it end square root" + } + FUNCTION_CALL -> { + val argStr = functionCall.argument.toHumanReadableEnglishString(divAsFraction) + when (functionCall.functionType) { + SQUARE_ROOT -> argStr?.let { + if (functionCall.argument.isSingleTerm()) { + resourceHandler.getStringInLocaleWithWrapping( + R.string.math_accessibility_simple_square_root, it + ) + } else { + resourceHandler.getStringInLocaleWithWrapping( + R.string.math_accessibility_complex_square_root, it + ) } - FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> null } + FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> null } - GROUP -> group.toHumanReadableEnglishString(divAsFraction)?.let { - if (isSingleTerm()) it else "open parenthesis $it close parenthesis" - } - EXPRESSIONTYPE_NOT_SET, null -> null } + GROUP -> group.toHumanReadableEnglishString(divAsFraction)?.let { + if (!isSingleTerm()) { + resourceHandler.getStringInLocaleWithWrapping(R.string.math_accessibility_group, it) + } else it + } + EXPRESSIONTYPE_NOT_SET, null -> null } + } + private companion object { private fun MathBinaryOperation.canBeReadAsImplicitMultiplication(): Boolean { // Note that exponentiation is specialized since it's higher precedence than multiplication // which means the graph won't look like "constant * variable" for polynomial terms like 2x^4 diff --git a/app/src/main/res/values/untranslated_strings.xml b/app/src/main/res/values/untranslated_strings.xml index 5b1ceec25ab..5494cfc9daf 100644 --- a/app/src/main/res/values/untranslated_strings.xml +++ b/app/src/main/res/values/untranslated_strings.xml @@ -41,4 +41,21 @@ Profile data is currently uploading… All profile data has been uploaded. Please connect to a WiFi or Cellular network in order to upload profile data. + + zed + one half + %s equals %s + %s plus %s + %s minus %s + %s times %s + %s divided by %s + %s raised to the power of %s + negative %s + positive %s + square root of %s + start square root %s end square root + open parenthesis %s close parenthesis + %s %s + %s over %s + the fraction with numerator %s and denominator %s diff --git a/app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt b/app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt index 835f8d78d19..c3ac67aa09b 100644 --- a/app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt +++ b/app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt @@ -424,6 +424,39 @@ class AppLanguageResourceHandlerTest { } } + @Test + fun testFormatLong_forLargeLong_returnsStringWithExactDigits() { + updateAppLanguageTo(OppiaLanguage.ENGLISH) + val handler = retrieveAppLanguageResourceHandler() + + val formattedString = handler.formatLong(123456789) + + assertThat(formattedString.filter { it.isDigit() }).isEqualTo("123456789") + } + + @Test + fun testFormatLong_forDouble_returnsStringWithExactDigits() { + updateAppLanguageTo(OppiaLanguage.ENGLISH) + val handler = retrieveAppLanguageResourceHandler() + + val formattedString = handler.formatDouble(454545456.123) + + val digitsOnly = formattedString.filter { it.isDigit() } + assertThat(digitsOnly).contains("454545456") + assertThat(digitsOnly).contains("123") + } + + @Test + fun testFormatLong_forDouble_returnsStringWithPeriodsOrCommas() { + updateAppLanguageTo(OppiaLanguage.ENGLISH) + val handler = retrieveAppLanguageResourceHandler() + + val formattedString = handler.formatDouble(123456789.123) + + // Depending on formatting, commas and/or periods are used for large doubles. + assertThat(formattedString).containsMatch("[,.]") + } + @Test fun testComputeDateString_forFixedTime_returnMonthDayYearParts() { updateAppLanguageTo(OppiaLanguage.ENGLISH) diff --git a/app/src/test/java/org/oppia/android/app/utility/math/BUILD.bazel b/app/src/test/java/org/oppia/android/app/utility/math/BUILD.bazel index 48908b50a8c..3f5ff80c424 100644 --- a/app/src/test/java/org/oppia/android/app/utility/math/BUILD.bazel +++ b/app/src/test/java/org/oppia/android/app/utility/math/BUILD.bazel @@ -13,19 +13,35 @@ oppia_android_test( test_manifest = "//app:test_manifest", deps = [ ":dagger", + "//app", + "//app/src/main/java/org/oppia/android/app/translation/testing:test_module", "//app/src/main/java/org/oppia/android/app/utility/math:math_expression_accessibility_util", + "//domain/src/main/java/org/oppia/android/domain/onboarding/testing:retriever_test_module", "//model/src/main/proto:languages_java_proto_lite", "//model/src/main/proto:math_java_proto_lite", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", + "//testing/src/main/java/org/oppia/android/testing/junit:initialize_default_locale_rule", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", "//third_party:androidx_test_core", "//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/accessibility:test_module", + "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module", + "//utility/src/main/java/org/oppia/android/util/caching/testing:caching_test_module", + "//utility/src/main/java/org/oppia/android/util/locale/testing:test_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", + "//utility/src/main/java/org/oppia/android/util/networking:debug_module", + "//utility/src/main/java/org/oppia/android/util/networking:debug_util_module", ], ) diff --git a/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt b/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt index baa7ea46a6e..23c929115e8 100644 --- a/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt +++ b/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt @@ -1,17 +1,29 @@ package org.oppia.android.app.utility.math import android.app.Application +import androidx.appcompat.app.AppCompatActivity import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.rules.ActivityScenarioRule import com.google.common.truth.StringSubject import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import dagger.BindsInstance import dagger.Component -import javax.inject.Inject import javax.inject.Singleton 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.activity.ActivityIntentFactoriesModule +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.MathBinaryOperation import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression @@ -27,6 +39,39 @@ import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED import org.oppia.android.app.model.Real +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.algebraicexpressioninput.AlgebraicExpressionInputModule +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.mathequationinput.MathEquationInputModule +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.numericexpressioninput.NumericExpressionInputModule +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.testing.ExpirationMetaDataRetrieverTestModule +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.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter @@ -37,12 +82,27 @@ import org.oppia.android.testing.math.MathEquationSubject import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat import org.oppia.android.testing.math.MathExpressionSubject import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat +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.testing.LocaleTestModule +import org.oppia.android.util.logging.LoggerModule +import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.REQUIRED_ONLY import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.ONE_HALF +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 @@ -61,13 +121,28 @@ import org.robolectric.annotation.LooperMode @LooperMode(LooperMode.Mode.PAUSED) @Config(application = MathExpressionAccessibilityUtilTest.TestApplication::class) class MathExpressionAccessibilityUtilTest { - @Inject lateinit var util: MathExpressionAccessibilityUtil + @get:Rule + val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() + + @get:Rule + var activityRule = + ActivityScenarioRule( + TestActivity.createIntent(ApplicationProvider.getApplicationContext()) + ) @Parameter lateinit var language: String @Parameter lateinit var expression: String @Parameter lateinit var equation: String @Parameter lateinit var a11yStr: String + lateinit var util: MathExpressionAccessibilityUtil + + @Before + fun setUp() { + setUpTestApplicationComponent() + activityRule.scenario.onActivity { util = it.mathExpressionAccessibilityUtil } + } + @Test fun testConvertToString_defaultExp_english_returnsNull() { val exp = MathExpression.getDefaultInstance() @@ -1177,11 +1252,6 @@ class MathExpressionAccessibilityUtilTest { assertThat(eq).forHumanReadable(ENGLISH).doesNotConvertToString() } - @Before - fun setUp() { - setUpTestApplicationComponent() - } - private fun MathExpressionSubject.forHumanReadable( language: OppiaLanguage ): HumanReadableStringChecker { @@ -1230,8 +1300,31 @@ class MathExpressionAccessibilityUtilTest { // TODO(#89): Move this to a common test application component. @Singleton - @Component - interface TestApplicationComponent { + @Component( + modules = [ + RobolectricModule::class, TestDispatcherModule::class, ApplicationModule::class, + PlatformParameterModule::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, ExpirationMetaDataRetrieverTestModule::class, + ViewBindingShimModule::class, RatioInputModule::class, NetworkConfigProdModule::class, + ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class, + LogUploadWorkerModule::class, WorkManagerConfigurationModule::class, + FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class, + DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, + ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, + NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, + AssetModule::class, LocaleTestModule::class, ActivityRecreatorTestModule::class, + ActivityIntentFactoriesModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class + ] + ) + interface TestApplicationComponent : ApplicationComponent { @Component.Builder interface Builder { @BindsInstance @@ -1243,7 +1336,7 @@ class MathExpressionAccessibilityUtilTest { fun inject(test: MathExpressionAccessibilityUtilTest) } - class TestApplication : Application() { + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { private val component: TestApplicationComponent by lazy { DaggerMathExpressionAccessibilityUtilTest_TestApplicationComponent.builder() .setApplication(this) @@ -1253,6 +1346,12 @@ class MathExpressionAccessibilityUtilTest { fun inject(test: MathExpressionAccessibilityUtilTest) { component.inject(test) } + + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() + } + + override fun getApplicationInjector(): ApplicationInjector = component } private companion object { diff --git a/domain/src/main/java/org/oppia/android/domain/locale/DisplayLocaleImpl.kt b/domain/src/main/java/org/oppia/android/domain/locale/DisplayLocaleImpl.kt index 39b231cf4ac..8d3a291b961 100644 --- a/domain/src/main/java/org/oppia/android/domain/locale/DisplayLocaleImpl.kt +++ b/domain/src/main/java/org/oppia/android/domain/locale/DisplayLocaleImpl.kt @@ -9,6 +9,7 @@ import org.oppia.android.app.model.OppiaLocaleContext import org.oppia.android.util.locale.OppiaBidiFormatter import org.oppia.android.util.locale.OppiaLocale import java.text.DateFormat +import java.text.NumberFormat import java.util.Date import java.util.Locale import java.util.Objects @@ -33,6 +34,7 @@ class DisplayLocaleImpl( private val dateTimeFormat by lazy { DateFormat.getDateTimeInstance(DATE_FORMAT_LENGTH, TIME_FORMAT_LENGTH, formattingLocale) } + private val numberFormat by lazy { NumberFormat.getNumberInstance(formattingLocale) } private val bidiFormatter by lazy { formatterFactory.createFormatter(formattingLocale) } // TODO(#3766): Restrict to be 'internal'. @@ -46,6 +48,10 @@ class DisplayLocaleImpl( configuration.setLocale(formattingLocale) } + override fun formatLong(value: Long): String = numberFormat.format(value) + + override fun formatDouble(value: Double): String = numberFormat.format(value) + override fun computeDateString(timestampMillis: Long): String = dateFormat.format(Date(timestampMillis)) diff --git a/domain/src/test/java/org/oppia/android/domain/locale/DisplayLocaleImplTest.kt b/domain/src/test/java/org/oppia/android/domain/locale/DisplayLocaleImplTest.kt index 0ff73fdb1b8..4a55dffc08d 100644 --- a/domain/src/test/java/org/oppia/android/domain/locale/DisplayLocaleImplTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/locale/DisplayLocaleImplTest.kt @@ -133,6 +133,36 @@ class DisplayLocaleImplTest { assertThat(impl2).isEqualTo(impl1) } + @Test + fun testFormatLong_forLargeLong_returnsStringWithExactDigits() { + val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT) + + val formattedString = impl.formatLong(123456789) + + assertThat(formattedString.filter { it.isDigit() }).isEqualTo("123456789") + } + + @Test + fun testFormatLong_forDouble_returnsStringWithExactDigits() { + val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT) + + val formattedString = impl.formatDouble(454545456.123) + + val digitsOnly = formattedString.filter { it.isDigit() } + assertThat(digitsOnly).contains("454545456") + assertThat(digitsOnly).contains("123") + } + + @Test + fun testFormatLong_forDouble_returnsStringWithPeriodsOrCommas() { + val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT) + + val formattedString = impl.formatDouble(123456789.123) + + // Depending on formatting, commas and/or periods are used for large doubles. + assertThat(formattedString).containsMatch("[,.]") + } + @Test fun testComputeDateString_forFixedTime_returnMonthDayYearParts() { val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT) diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index 70a2d35df7a..f0d04f694f8 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -286,6 +286,7 @@ file_content_checks { file_path_regex: ".+?\\.kt" prohibited_content_regex: "OppiaParameterizedTestRunner" failure_message: "To use OppiaParameterizedTestRunner, please add an exemption to file_content_validation_checks.textproto and add an explanation for your use case in your PR description. Note that parameterized tests should only be used in special circumstances where a single behavior can be tested across multiple inputs, or for especially large test suites that can be trivially reduced." + exempted_file_name: "app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt" diff --git a/utility/src/main/java/org/oppia/android/util/locale/OppiaLocale.kt b/utility/src/main/java/org/oppia/android/util/locale/OppiaLocale.kt index b4aa0fa3439..0011ca6872f 100644 --- a/utility/src/main/java/org/oppia/android/util/locale/OppiaLocale.kt +++ b/utility/src/main/java/org/oppia/android/util/locale/OppiaLocale.kt @@ -207,6 +207,25 @@ sealed class OppiaLocale { * [org.oppia.android.domain.locale.LocaleController.setAsDefault]). */ abstract class DisplayLocale(override val localeContext: OppiaLocaleContext) : OppiaLocale() { + /** + * Returns a locally formatted representation of the long integer [value]. + * + * No assumptions can be made regarding the formatting of the returned string except that: + * 1. The exact value will be represented (no rounding or truncation will occur). + * 2. The resulting value should be generally readable by screenreaders if they support the the + * current locale. + */ + abstract fun formatLong(value: Long): String + + /** + * Returns a locally formatted representation of the double [value]. + * + * No assumptions can be made regarding the formatting of the returned string except that it + * should generally be readable by screenreaders if they support the current locale. This + * function may round and/or truncate the double for formatting simplicity. + */ + abstract fun formatDouble(value: Double): String + /** * Returns a locally formatted date string representing the specified Unix timestamp. * From 01b18321b9663577d8e632ab13dfa5595e572535 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 7 Feb 2022 22:36:51 -0800 Subject: [PATCH 121/134] Lint fixes. --- .../org/oppia/android/app/testing/activity/TestActivity.kt | 2 +- .../app/utility/math/MathExpressionAccessibilityUtil.kt | 6 +++--- .../app/utility/math/MathExpressionAccessibilityUtilTest.kt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/testing/activity/TestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/activity/TestActivity.kt index 72be82faf23..605c5699f69 100644 --- a/app/src/main/java/org/oppia/android/app/testing/activity/TestActivity.kt +++ b/app/src/main/java/org/oppia/android/app/testing/activity/TestActivity.kt @@ -7,8 +7,8 @@ import org.oppia.android.app.activity.InjectableAppCompatActivity import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.translation.AppLanguageWatcherMixin import org.oppia.android.app.utility.datetime.DateTimeUtil -import javax.inject.Inject import org.oppia.android.app.utility.math.MathExpressionAccessibilityUtil +import javax.inject.Inject // TODO(#3830): Migrate all test activities over to using this test activity & make this closed. /** diff --git a/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt b/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt index 5001922b9ab..6995cf901b1 100644 --- a/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt +++ b/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt @@ -1,9 +1,7 @@ package org.oppia.android.app.utility.math import org.oppia.android.R -import javax.inject.Inject import org.oppia.android.app.model.MathBinaryOperation -import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE @@ -21,7 +19,6 @@ import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE import org.oppia.android.app.model.MathFunctionCall.FunctionType import org.oppia.android.app.model.MathFunctionCall.FunctionType.FUNCTION_UNSPECIFIED import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT -import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE import org.oppia.android.app.model.OppiaLanguage @@ -38,6 +35,9 @@ import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET import org.oppia.android.app.translation.AppLanguageResourceHandler +import javax.inject.Inject +import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator /** * Utility for computing an accessibility string for screenreaders to be able to read out parsed diff --git a/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt b/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt index 23c929115e8..b9c58dc7a09 100644 --- a/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt +++ b/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt @@ -9,7 +9,6 @@ import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import dagger.BindsInstance import dagger.Component -import javax.inject.Singleton import org.junit.Before import org.junit.Rule import org.junit.Test @@ -105,6 +104,7 @@ 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 [MathExpressionAccessibilityUtil]. From a6dc7d42a6a39fa9065b632c32e7a8b3992d402b Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 18 Feb 2022 11:01:21 -0800 Subject: [PATCH 122/134] Address reviewer comment. Clarifies the documentation in the test runner around parameter injection. --- .../testing/junit/OppiaParameterizedTestRunner.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt index 6638a801db0..10a446665d5 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt @@ -29,22 +29,25 @@ import kotlin.reflect.KClass * * To introduce parameterized tests, add this runner along with one or more [Parameter]-annotated * fields and one or more [RunParameterized]-annotated methods (where each method should have - * multiple [Iteration]s defined to describe each test iteration). Here's a simple example: + * multiple [Iteration]s defined to describe each test iteration). Note that only strings and + * primitive types (e.g. [Int], [Long], [Float], [Double], and [Boolean]) are supported for + * parameter injection. Here's a simple example: * * ```kotlin * @RunWith(OppiaParameterizedTestRunner::class) * @SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) * class ExampleParameterizedTest { - * @Parameter lateinit var parameter: String + * @Parameter lateinit var strParam: String + * @Parameter var intParam: Int = Int.MIN_VALUE // Inited because primitives can't be lateinit. * * @Test * @RunParameterized( - * Iteration("first", "parameter=first value"), - * Iteration("second", "parameter=second value"), - * Iteration("third", "parameter=third value") + * Iteration("first", "strParam=first value", "intParam=12"), + * Iteration("second", "strParam=second value", "intParam=-72"), + * Iteration("third", "strParam=third value", "intParam=15") * ) * fun testParams_multipleVals_isConsistent() { - * val result = performOperation(parameter) + * val result = performOperation(strParam, intParam) * assertThat(result).isEqualTo(consistentExpectedValue) * } * } From fd9ec1f3c402714c539e16659fac6c93a1ffacbe Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 18 Feb 2022 13:20:00 -0800 Subject: [PATCH 123/134] Fix broken build. --- .../org/oppia/android/domain/classify/BUILD.bazel | 12 ++++++------ .../oppia/android/domain/classify/rules/BUILD.bazel | 6 +++--- .../classify/rules/dragAndDropSortInput/BUILD.bazel | 4 ++-- .../domain/classify/rules/fractioninput/BUILD.bazel | 4 ++-- .../classify/rules/imageClickInput/BUILD.bazel | 4 ++-- .../classify/rules/itemselectioninput/BUILD.bazel | 4 ++-- .../classify/rules/multiplechoiceinput/BUILD.bazel | 4 ++-- .../classify/rules/numberwithunits/BUILD.bazel | 4 ++-- .../domain/classify/rules/numericinput/BUILD.bazel | 4 ++-- .../domain/classify/rules/ratioinput/BUILD.bazel | 4 ++-- .../domain/classify/rules/textinput/BUILD.bazel | 6 +++--- 11 files changed, 28 insertions(+), 28 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/BUILD.bazel index 6b7d7c7fe59..b2222c9b536 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/BUILD.bazel @@ -14,9 +14,9 @@ kt_android_library( deps = [ ":classification_result", ":interaction_classifier", - "//model:exploration_java_proto_lite", - "//model:interaction_object_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:exploration_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) @@ -28,7 +28,7 @@ kt_android_library( ], visibility = ["//:oppia_api_visibility"], deps = [ - "//model:exploration_java_proto_lite", + "//model/src/main/proto:exploration_java_proto_lite", ], ) @@ -77,8 +77,8 @@ kt_android_library( ], visibility = ["//:__subpackages__"], deps = [ - "//model:interaction_object_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/BUILD.bazel index 95880ffd7c6..f581741e245 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/BUILD.bazel @@ -15,8 +15,8 @@ kt_android_library( deps = [ ":rule_classifier_provider", "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", - "//model:interaction_object_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) @@ -30,7 +30,7 @@ kt_android_library( visibility = ["//:__subpackages__"], deps = [ "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", - "//model:translation_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/BUILD.bazel index dfed4bcfc94..b77cd0d460b 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/BUILD.bazel @@ -19,8 +19,8 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", "//domain/src/main/java/org/oppia/android/domain/util:extensions", - "//model:interaction_object_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/BUILD.bazel index 0de2f2b01a3..796ebb8680f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/BUILD.bazel @@ -25,8 +25,8 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", "//domain/src/main/java/org/oppia/android/domain/util:extensions", - "//model:interaction_object_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/BUILD.bazel index a6edda847b8..d31ece53126 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/BUILD.bazel @@ -16,8 +16,8 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", "//domain/src/main/java/org/oppia/android/domain/util:extensions", - "//model:interaction_object_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/BUILD.bazel index 0827ee61f10..a17f0aeed77 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/BUILD.bazel @@ -19,8 +19,8 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", "//domain/src/main/java/org/oppia/android/domain/util:extensions", - "//model:interaction_object_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/BUILD.bazel index 62b9e517e08..40560cba8cd 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/BUILD.bazel @@ -16,8 +16,8 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", "//domain/src/main/java/org/oppia/android/domain/util:extensions", - "//model:interaction_object_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/BUILD.bazel index f3e3b8c5c38..93b26be66de 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/BUILD.bazel @@ -17,8 +17,8 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", "//domain/src/main/java/org/oppia/android/domain/util:extensions", - "//model:interaction_object_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/BUILD.bazel index 0276f06dec7..16eb2cb8cb3 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/BUILD.bazel @@ -22,8 +22,8 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", "//domain/src/main/java/org/oppia/android/domain/util:extensions", - "//model:interaction_object_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/BUILD.bazel index 042ed4652a6..9ae12eba548 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/BUILD.bazel @@ -19,8 +19,8 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", "//domain/src/main/java/org/oppia/android/domain/util:extensions", - "//model:interaction_object_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_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 4acd72a6675..0701b29ff46 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 @@ -20,9 +20,9 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", "//domain/src/main/java/org/oppia/android/domain/translation:translation_controller", "//domain/src/main/java/org/oppia/android/domain/util:extensions", - "//model:exploration_java_proto_lite", - "//model:interaction_object_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:exploration_java_proto_lite", + "//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/locale:oppia_locale", ], From 0fc8a1b81df9a5c0537da6c7ffdc88a008f59669 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 18 Feb 2022 13:30:14 -0800 Subject: [PATCH 124/134] Fix broken build post-merge. --- .../oppia/android/domain/classify/rules/textinput/BUILD.bazel | 1 + 1 file changed, 1 insertion(+) 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", ], ) From c59598c9e5ae60b5a120d2a4f2fcb05158a66e15 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 18 Feb 2022 13:51:58 -0800 Subject: [PATCH 125/134] Fix broken post-merge classifier. --- .../rules/numericexpressioninput/BUILD.bazel | 46 +++++++++++++++++++ .../rules/numericexpressioninput/BUILD.bazel | 4 ++ 2 files changed, 50 insertions(+) create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel new file mode 100644 index 00000000000..f8a8eb828e3 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel @@ -0,0 +1,46 @@ +""" +Classifiers for the 'NumericExpressionInput' interaction. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +kt_android_library( + name = "numeric_expression_input_providers", + srcs = [ + "NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt", + "NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt", + "NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt", + ], + visibility = ["//domain:domain_testing_visibility"], + deps = [ + ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", + "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", + "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", + "//model/src/main/proto:exploration_java_proto_lite", + "//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/locale:oppia_locale", + "//utility/src/main/java/org/oppia/android/util/logging:console_logger", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", + ], +) + +kt_android_library( + name = "numeric_expression_input_rule_module", + srcs = [ + "NumericExpressionInputModule.kt", + ], + visibility = ["//:oppia_prod_module_visibility"], + deps = [ + ":dagger", + ":numeric_expression_input_providers", + "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", + "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", + ], +) + +dagger_rules() diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel index f757f37b29e..7755b5ca94a 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel @@ -14,6 +14,7 @@ oppia_android_test( deps = [ ":dagger", "//domain", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput:numeric_expression_input_providers", "//model/src/main/proto:interaction_object_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", @@ -39,6 +40,7 @@ oppia_android_test( deps = [ ":dagger", "//domain", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput:numeric_expression_input_providers", "//model/src/main/proto:interaction_object_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", @@ -64,6 +66,7 @@ oppia_android_test( deps = [ ":dagger", "//domain", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput:numeric_expression_input_providers", "//model/src/main/proto:interaction_object_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", @@ -89,6 +92,7 @@ oppia_android_test( deps = [ ":dagger", "//domain", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput:numeric_expression_input_rule_module", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", "//testing/src/main/java/org/oppia/android/testing/threading:test_module", "//testing/src/main/java/org/oppia/android/testing/time:test_module", From 41be40ee97ce84c818dfc1928d37b14d6bd36b06 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 18 Feb 2022 13:55:31 -0800 Subject: [PATCH 126/134] Address reviewer comment. --- .../android/domain/classify/AnswerClassificationController.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/AnswerClassificationController.kt b/domain/src/main/java/org/oppia/android/domain/classify/AnswerClassificationController.kt index 55eb6023584..979b8a28fc6 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/AnswerClassificationController.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/AnswerClassificationController.kt @@ -36,7 +36,6 @@ class AnswerClassificationController @Inject constructor( "expected one of: ${interactionClassifiers.keys}" } // TODO(#207): Add support for additional classification types. - interaction.customizationArgsMap return classifyAnswer( answer, interaction.answerGroupsList, From 1ea22417c6b34ab900d26e45c5c36fd0da6e3574 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 18 Feb 2022 14:06:08 -0800 Subject: [PATCH 127/134] Post-merge build fixes. --- .../oppia/android/domain/classify/BUILD.bazel | 15 ++++++ .../algebraicexpressioninput/BUILD.bazel | 47 +++++++++++++++++++ .../rules/dragAndDropSortInput/BUILD.bazel | 1 + .../classify/rules/fractioninput/BUILD.bazel | 1 + .../rules/imageClickInput/BUILD.bazel | 1 + .../rules/itemselectioninput/BUILD.bazel | 1 + .../rules/multiplechoiceinput/BUILD.bazel | 1 + .../rules/numberwithunits/BUILD.bazel | 1 + .../rules/numericexpressioninput/BUILD.bazel | 1 + .../classify/rules/numericinput/BUILD.bazel | 1 + .../classify/rules/ratioinput/BUILD.bazel | 1 + .../classify/rules/textinput/BUILD.bazel | 1 + .../algebraicexpressioninput/BUILD.bazel | 4 ++ 13 files changed, 76 insertions(+) create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/BUILD.bazel diff --git a/domain/src/main/java/org/oppia/android/domain/classify/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/BUILD.bazel index b2222c9b536..7c523f15e2f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/BUILD.bazel @@ -12,6 +12,7 @@ kt_android_library( ], visibility = ["//:oppia_api_visibility"], deps = [ + ":classification_context", ":classification_result", ":interaction_classifier", "//model/src/main/proto:exploration_java_proto_lite", @@ -21,6 +22,18 @@ kt_android_library( ], ) +kt_android_library( + name = "classification_context", + srcs = [ + "ClassificationContext.kt", + ], + visibility = ["//:oppia_api_visibility"], + deps = [ + "//model/src/main/proto:exploration_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", + ], +) + kt_android_library( name = "classification_result", srcs = [ @@ -38,6 +51,7 @@ kt_android_library( "GenericInteractionClassifier.kt", ], deps = [ + ":classification_context", ":interaction_classifier", ":rule_classifier", ], @@ -77,6 +91,7 @@ kt_android_library( ], visibility = ["//:__subpackages__"], deps = [ + ":classification_context", "//model/src/main/proto:interaction_object_java_proto_lite", "//model/src/main/proto:translation_java_proto_lite", ], diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/BUILD.bazel new file mode 100644 index 00000000000..c59365719f5 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/BUILD.bazel @@ -0,0 +1,47 @@ +""" +Classifiers for the 'AlgebraicExpressionInput' interaction. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +kt_android_library( + name = "algebraic_expression_input_providers", + srcs = [ + "AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt", + "AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt", + "AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt", + ], + visibility = ["//domain:domain_testing_visibility"], + deps = [ + ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:classification_context", + "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", + "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", + "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", + "//model/src/main/proto:exploration_java_proto_lite", + "//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/locale:oppia_locale", + "//utility/src/main/java/org/oppia/android/util/logging:console_logger", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", + ], +) + +kt_android_library( + name = "algebraic_expression_input_rule_module", + srcs = [ + "AlgebraicExpressionInputModule.kt", + ], + visibility = ["//:oppia_prod_module_visibility"], + deps = [ + ":algebraic_expression_input_providers", + ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", + "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", + ], +) + +dagger_rules() diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/BUILD.bazel index b77cd0d460b..5dadd526a23 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/BUILD.bazel @@ -15,6 +15,7 @@ kt_android_library( ], deps = [ ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:classification_context", "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/BUILD.bazel index 796ebb8680f..5da32a18e7c 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/BUILD.bazel @@ -21,6 +21,7 @@ kt_android_library( ], deps = [ ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:classification_context", "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/BUILD.bazel index d31ece53126..eb216ecc013 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/BUILD.bazel @@ -12,6 +12,7 @@ kt_android_library( ], deps = [ ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:classification_context", "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/BUILD.bazel index a17f0aeed77..b1ae3ef0a1f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/BUILD.bazel @@ -15,6 +15,7 @@ kt_android_library( ], deps = [ ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:classification_context", "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/BUILD.bazel index 40560cba8cd..19a34a097ca 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/BUILD.bazel @@ -12,6 +12,7 @@ kt_android_library( ], deps = [ ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:classification_context", "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/BUILD.bazel index 93b26be66de..1954b42da92 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/BUILD.bazel @@ -13,6 +13,7 @@ kt_android_library( ], deps = [ ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:classification_context", "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel index f8a8eb828e3..5a11936c237 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel @@ -15,6 +15,7 @@ kt_android_library( visibility = ["//domain:domain_testing_visibility"], deps = [ ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:classification_context", "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/BUILD.bazel index 16eb2cb8cb3..2d8f8e7136e 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/BUILD.bazel @@ -18,6 +18,7 @@ kt_android_library( ], deps = [ ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:classification_context", "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/BUILD.bazel index 9ae12eba548..6fb01de8afb 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/BUILD.bazel @@ -15,6 +15,7 @@ kt_android_library( ], deps = [ ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:classification_context", "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", 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 a28d873bc5a..4a8a59451fa 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 @@ -15,6 +15,7 @@ kt_android_library( ], deps = [ ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:classification_context", "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/BUILD.bazel index a9bacbd93ba..786cd962d4d 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/BUILD.bazel +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/BUILD.bazel @@ -14,6 +14,7 @@ oppia_android_test( deps = [ ":dagger", "//domain", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput:algebraic_expression_input_providers", "//model/src/main/proto:interaction_object_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", @@ -39,6 +40,7 @@ oppia_android_test( deps = [ ":dagger", "//domain", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput:algebraic_expression_input_providers", "//model/src/main/proto:interaction_object_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", @@ -64,6 +66,7 @@ oppia_android_test( deps = [ ":dagger", "//domain", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput:algebraic_expression_input_providers", "//model/src/main/proto:interaction_object_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", @@ -89,6 +92,7 @@ oppia_android_test( deps = [ ":dagger", "//domain", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput:algebraic_expression_input_rule_module", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", "//testing/src/main/java/org/oppia/android/testing/threading:test_module", "//testing/src/main/java/org/oppia/android/testing/time:test_module", From 3407bf47ef7b3122c499dbfcb0d0be205feb33dd Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 18 Feb 2022 14:11:31 -0800 Subject: [PATCH 128/134] Post-merge build fixes for new classifiers. --- .../rules/mathequationinput/BUILD.bazel | 47 +++++++++++++++++++ .../rules/mathequationinput/BUILD.bazel | 4 ++ 2 files changed, 51 insertions(+) create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/BUILD.bazel diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/BUILD.bazel new file mode 100644 index 00000000000..0a70ea19348 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/BUILD.bazel @@ -0,0 +1,47 @@ +""" +Classifiers for the 'MathEquationInput' interaction. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +kt_android_library( + name = "math_equation_input_providers", + srcs = [ + "MathEquationInputIsEquivalentToRuleClassifierProvider.kt", + "MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt", + "MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt", + ], + visibility = ["//domain:domain_testing_visibility"], + deps = [ + ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:classification_context", + "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", + "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", + "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", + "//model/src/main/proto:exploration_java_proto_lite", + "//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/locale:oppia_locale", + "//utility/src/main/java/org/oppia/android/util/logging:console_logger", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", + ], +) + +kt_android_library( + name = "math_equation_input_rule_module", + srcs = [ + "MathEquationInputModule.kt", + ], + visibility = ["//:oppia_prod_module_visibility"], + deps = [ + ":dagger", + ":math_equation_input_providers", + "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", + "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", + ], +) + +dagger_rules() diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/BUILD.bazel index 9cfc129f250..88686b47484 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/BUILD.bazel +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/BUILD.bazel @@ -14,6 +14,7 @@ oppia_android_test( deps = [ ":dagger", "//domain", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput:math_equation_input_providers", "//model/src/main/proto:interaction_object_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", @@ -39,6 +40,7 @@ oppia_android_test( deps = [ ":dagger", "//domain", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput:math_equation_input_providers", "//model/src/main/proto:interaction_object_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", @@ -64,6 +66,7 @@ oppia_android_test( deps = [ ":dagger", "//domain", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput:math_equation_input_providers", "//model/src/main/proto:interaction_object_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", @@ -89,6 +92,7 @@ oppia_android_test( deps = [ ":dagger", "//domain", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput:math_equation_input_rule_module", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", "//testing/src/main/java/org/oppia/android/testing/threading:test_module", "//testing/src/main/java/org/oppia/android/testing/time:test_module", From 4328d564ce1352ff5ff70ecc365ac4a0a4754b47 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 18 Feb 2022 15:10:32 -0800 Subject: [PATCH 129/134] Post-merge build fixes. --- app/BUILD.bazel | 6 ++++++ domain/BUILD.bazel | 3 +++ .../java/org/oppia/android/testing/junit/BUILD.bazel | 9 +++++++++ 3 files changed, 18 insertions(+) diff --git a/app/BUILD.bazel b/app/BUILD.bazel index 1946d16f34d..88637d4fdfa 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -733,13 +733,16 @@ kt_android_library( "//data/src/main/java/org/oppia/android/data/backends/gae:network_config_prod_module", "//data/src/main/java/org/oppia/android/data/backends/gae:prod_module", "//domain/src/main/java/org/oppia/android/domain/classify:interactions_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput:algebraic_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/continueinteraction:continue_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput:drag_and_drop_sort_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput:fraction_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput:image_click_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput:item_selection_input_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput:math_equation_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput:multiple_choice_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits:number_with_units_rule_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput:numeric_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput:numeric_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput:ratio_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/textinput:text_input_rule_module", @@ -825,13 +828,16 @@ TEST_DEPS = [ "//app/src/main/java/org/oppia/android/app/translation/testing:test_module", "//domain", "//domain/src/main/java/org/oppia/android/domain/audio:audio_player_controller", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput:algebraic_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/continueinteraction:continue_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput:drag_and_drop_sort_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput:fraction_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput:image_click_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput:item_selection_input_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput:math_equation_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput:multiple_choice_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits:number_with_units_rule_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput:numeric_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput:numeric_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput:ratio_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/textinput:text_input_rule_module", diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index d02df93f2ef..ef961b09449 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -173,13 +173,16 @@ TEST_DEPS = [ "//domain/src/main/java/org/oppia/android/domain/audio:audio_player_controller", "//domain/src/main/java/org/oppia/android/domain/audio:cellular_audio_dialog_controller", "//domain/src/main/java/org/oppia/android/domain/classify:interactions_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput:algebraic_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/continueinteraction:continue_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput:drag_and_drop_sort_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput:fraction_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput:image_click_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput:item_selection_input_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput:math_equation_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput:multiple_choice_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits:number_with_units_rule_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput:numeric_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput:numeric_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput:ratio_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/textinput:text_input_rule_module", diff --git a/testing/src/test/java/org/oppia/android/testing/junit/BUILD.bazel b/testing/src/test/java/org/oppia/android/testing/junit/BUILD.bazel index 6c09229ee41..afae4ea110d 100644 --- a/testing/src/test/java/org/oppia/android/testing/junit/BUILD.bazel +++ b/testing/src/test/java/org/oppia/android/testing/junit/BUILD.bazel @@ -18,13 +18,16 @@ oppia_android_test( "//app/src/main/java/org/oppia/android/app/translation/testing:test_module", "//domain", "//domain/src/main/java/org/oppia/android/domain/classify:interactions_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput:algebraic_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/continueinteraction:continue_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput:drag_and_drop_sort_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput:fraction_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput:image_click_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput:item_selection_input_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput:math_equation_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput:multiple_choice_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits:number_with_units_rule_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput:numeric_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput:numeric_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput:ratio_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/textinput:text_input_rule_module", @@ -63,13 +66,16 @@ oppia_android_test( "//app/src/main/java/org/oppia/android/app/translation/testing:test_module", "//domain", "//domain/src/main/java/org/oppia/android/domain/classify:interactions_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput:algebraic_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/continueinteraction:continue_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput:drag_and_drop_sort_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput:fraction_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput:image_click_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput:item_selection_input_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput:math_equation_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput:multiple_choice_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits:number_with_units_rule_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput:numeric_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput:numeric_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput:ratio_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/textinput:text_input_rule_module", @@ -107,13 +113,16 @@ oppia_android_test( "//app/src/main/java/org/oppia/android/app/translation/testing:test_module", "//domain", "//domain/src/main/java/org/oppia/android/domain/classify:interactions_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput:algebraic_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/continueinteraction:continue_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput:drag_and_drop_sort_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput:fraction_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput:image_click_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput:item_selection_input_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput:math_equation_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput:multiple_choice_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits:number_with_units_rule_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput:numeric_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput:numeric_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput:ratio_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/textinput:text_input_rule_module", From 0e9402a15ea1ba88d036098caf142f7ec8ea08ae Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 18 Feb 2022 15:27:13 -0800 Subject: [PATCH 130/134] Correct reference document link. --- .../app/utility/math/MathExpressionAccessibilityUtil.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt b/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt index 6995cf901b1..3f6c1249efa 100644 --- a/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt +++ b/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt @@ -106,8 +106,7 @@ class MathExpressionAccessibilityUtil @Inject constructor( } private fun MathExpression.toHumanReadableEnglishString(divAsFraction: Boolean): String? { - // Reference: - // https://docs.google.com/document/d/1P-dldXQ08O-02ZRG978paiWOSz0dsvcKpDgiV_rKH_Y/view. + // Ref: https://docs.google.com/document/d/1SkzAD4k7SWLp5_3L5WNxsnR79ATlOk8pz4irfE2ls-4/view. // Note that extra bidi wrapping is occurring here since there's not an obvious way to wrap "at // the end" for non-equations. From 8691e43e2ed7e4d9a2aa473c69aa5b3fa8c02a42 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 23 Feb 2022 18:18:46 -0800 Subject: [PATCH 131/134] Add and fix missing test (was broken on Gradle). --- .../oppia/android/app/databinding/BUILD.bazel | 41 +++++++++++++++++++ .../TextViewBindingAdaptersTest.kt | 7 +++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/BUILD.bazel b/app/src/sharedTest/java/org/oppia/android/app/databinding/BUILD.bazel index 341b24b5d13..1279ddff613 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/BUILD.bazel +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/BUILD.bazel @@ -51,6 +51,17 @@ genrule( """, ) +genrule( + name = "update_TextViewBindingAdaptersTest", + srcs = ["TextViewBindingAdaptersTest.kt"], + outs = ["TextViewBindingAdaptersTest_updated.kt"], + cmd = """ + cat $(SRCS) | + sed 's/import org.oppia.android.R/import org.oppia.android.app.databinding.R/g' | + sed 's/import org.oppia.android.app.databinding.TextViewBindingAdapters./import org.oppia.android.app.databinding.TextViewBindingAdapters_updated./g' > $(OUTS) + """, +) + oppia_android_test( name = "DrawableBindingAdaptersTest", srcs = ["DrawableBindingAdaptersTest_updated.kt"], @@ -201,6 +212,36 @@ oppia_android_test( ], ) +oppia_android_test( + name = "TextViewBindingAdaptersTest", + srcs = ["TextViewBindingAdaptersTest_updated.kt"], + custom_package = "org.oppia.android.app.databinding", + test_class = "org.oppia.android.app.databinding.TextViewBindingAdaptersTest", + test_manifest = "//app:test_manifest", + deps = [ + ":dagger", + "//app", + "//app:test_deps", + "//app/src/main/java/org/oppia/android/app/activity:activity_intent_factories_shim", + "//app/src/main/java/org/oppia/android/app/translation/testing:test_module", + "//domain", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/junit:initialize_default_locale_rule", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_ext_junit", + "//third_party:androidx_test_ext_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/accessibility:test_module", + "//utility/src/main/java/org/oppia/android/util/caching/testing:caching_test_module", + "//utility/src/main/java/org/oppia/android/util/data:data_providers", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + oppia_android_test( name = "ViewBindingAdaptersTest", srcs = ["ViewBindingAdaptersTest_updated.kt"], diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/TextViewBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/TextViewBindingAdaptersTest.kt index 0586b7ef7ef..46fe147948b 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/TextViewBindingAdaptersTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/TextViewBindingAdaptersTest.kt @@ -36,13 +36,16 @@ 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.algebraicexpressioninput.AlgebraicExpressionInputModule 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.mathequationinput.MathEquationInputModule 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.numericexpressioninput.NumericExpressionInputModule 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 @@ -320,7 +323,9 @@ class TextViewBindingAdaptersTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { From 391cf222f11eed6d2b6ecd1a4b073dfa62d896a3 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 17 Mar 2022 14:26:53 -0700 Subject: [PATCH 132/134] Post-merge fix. --- model/src/main/proto/BUILD.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/model/src/main/proto/BUILD.bazel b/model/src/main/proto/BUILD.bazel index 90da6e48b77..b38796c5237 100644 --- a/model/src/main/proto/BUILD.bazel +++ b/model/src/main/proto/BUILD.bazel @@ -37,6 +37,7 @@ java_lite_proto_library( oppia_proto_library( name = "event_logger_proto", srcs = ["oppia_logger.proto"], + deps = [":exploration_proto"], ) java_lite_proto_library( From c29a47c7b9c1354df839ef85075e7f55d6aea016 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 17 Mar 2022 14:45:25 -0700 Subject: [PATCH 133/134] More post-merge fixes. --- .../app/parser/FractionParsingUiErrorTest.kt | 21 ++++++++++--------- .../android/util/math/FractionParserTest.kt | 3 +-- 2 files changed, 12 insertions(+), 12 deletions(-) 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 index c3e2a1c805f..df39f6a196e 100644 --- a/app/src/test/java/org/oppia/android/app/parser/FractionParsingUiErrorTest.kt +++ b/app/src/test/java/org/oppia/android/app/parser/FractionParsingUiErrorTest.kt @@ -157,6 +157,17 @@ class FractionParsingUiErrorTest { } } + @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 -> @@ -211,16 +222,6 @@ class FractionParsingUiErrorTest { } } - @Test - fun testSubmitTimeError_noDenominator_invalidFormat_hasRelevantErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = fractionParser.getSubmitTimeError("3/") - .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) } 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 index 27988ea1e0e..864a429f753 100644 --- a/utility/src/test/java/org/oppia/android/util/math/FractionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/FractionParserTest.kt @@ -6,7 +6,6 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.Fraction -import org.oppia.android.app.parser.StringToFractionParser import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode @@ -94,7 +93,7 @@ class FractionParserTest { @Test fun testSubmitTimeError_noDenominator_returnsInvalidFormat() { val error = fractionParser.getSubmitTimeError("3/") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.INVALID_FORMAT) + assertThat(error).isEqualTo(FractionParser.FractionParsingError.INVALID_FORMAT) } @Test From 445664a36757bebf45c9c960ff507ca70f053508 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 17 Mar 2022 14:48:44 -0700 Subject: [PATCH 134/134] Fix TODO comment. --- .../org/oppia/android/testing/math/MathParsingErrorSubject.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt index 53c8bad235f..9c62f183aca 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt @@ -43,7 +43,7 @@ import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError -// TODO(#4132): file issue to add tests. +// TODO(#4132): Add tests for this class. /** * Truth subject for verifying properties of [MathParsingError]s.