From 16483d65744488146e96d149b41f5869a2597a4c Mon Sep 17 00:00:00 2001 From: Hannes Schulze Date: Mon, 1 Jul 2024 17:00:08 +0200 Subject: add quick answers --- flake.lock | 6 +- flake.nix | 1 + res/icons/fluent-calculator-symbolic.svg | 3 + res/icons/icons.gresource.xml | 1 + src/application.c | 5 + src/launcher-item.c | 73 +++++++ src/launcher-item.h | 28 +++ src/launcher.c | 83 +++++--- src/launcher.h | 2 + src/meson.build | 1 + src/services/application-service.c | 213 +++++++++++---------- src/services/application-service.h | 4 - src/services/meson.build | 1 + src/services/quick-answer-service.c | 317 +++++++++++++++++++++++++++++++ src/services/quick-answer-service.h | 21 ++ 15 files changed, 625 insertions(+), 134 deletions(-) create mode 100644 res/icons/fluent-calculator-symbolic.svg create mode 100644 src/launcher-item.c create mode 100644 src/launcher-item.h create mode 100644 src/services/quick-answer-service.c create mode 100644 src/services/quick-answer-service.h diff --git a/flake.lock b/flake.lock index 07758f2..5005946 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1714906307, - "narHash": "sha256-UlRZtrCnhPFSJlDQE7M0eyhgvuuHBTe1eJ9N9AQlJQ0=", + "lastModified": 1719506693, + "narHash": "sha256-C8e9S7RzshSdHB7L+v9I51af1gDM5unhJ2xO1ywxNH8=", "owner": "nixos", "repo": "nixpkgs", - "rev": "25865a40d14b3f9cf19f19b924e2ab4069b09588", + "rev": "b2852eb9365c6de48ffb0dc2c9562591f652242a", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 54b91fb..2468202 100644 --- a/flake.nix +++ b/flake.nix @@ -30,6 +30,7 @@ pkgs.clang-tools ]; inputsFrom = [ foobar ]; + hardeningDisable = [ "fortify" ]; }; }; }); diff --git a/res/icons/fluent-calculator-symbolic.svg b/res/icons/fluent-calculator-symbolic.svg new file mode 100644 index 0000000..21b9799 --- /dev/null +++ b/res/icons/fluent-calculator-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/icons/icons.gresource.xml b/res/icons/icons.gresource.xml index 29cab10..a5061ca 100644 --- a/res/icons/icons.gresource.xml +++ b/res/icons/icons.gresource.xml @@ -48,5 +48,6 @@ fluent-chevron-right-symbolic.svg fluent-checkmark-symbolic.svg fluent-microphone-symbolic.svg + fluent-calculator-symbolic.svg diff --git a/src/application.c b/src/application.c index a32e1ce..bad62b1 100644 --- a/src/application.c +++ b/src/application.c @@ -7,6 +7,7 @@ #include "services/battery-service.h" #include "services/clock-service.h" #include "services/brightness-service.h" +#include "services/quick-answer-service.h" #include "services/workspace-service.h" #include "services/notification-service.h" #include "services/audio-service.h" @@ -38,6 +39,7 @@ struct _FoobarApplication FoobarNetworkService* network_service; FoobarBluetoothService* bluetooth_service; FoobarApplicationService* application_service; + FoobarQuickAnswerService* quick_answer_service; FoobarConfigurationService* configuration_service; gulong config_handler_id; FoobarServer* server_skeleton; @@ -180,6 +182,7 @@ void foobar_application_activate( GApplication* app ) self->network_service = foobar_network_service_new( ); self->bluetooth_service = foobar_bluetooth_service_new( ); self->application_service = foobar_application_service_new( ); + self->quick_answer_service = foobar_quick_answer_service_new( ); self->configuration_service = foobar_configuration_service_new( ); // Enforce a uniform style by forcing Adwaita and shipping our own icons. @@ -231,6 +234,7 @@ void foobar_application_activate( GApplication* app ) self->launcher = foobar_launcher_new( self->application_service, + self->quick_answer_service, self->configuration_service ); g_object_ref( self->launcher ); @@ -358,6 +362,7 @@ void foobar_application_finalize( GObject* object ) g_clear_object( &self->network_service ); g_clear_object( &self->bluetooth_service ); g_clear_object( &self->application_service ); + g_clear_object( &self->quick_answer_service ); g_clear_object( &self->configuration_service ); g_clear_object( &self->style_provider ); g_clear_object( &self->server_skeleton ); diff --git a/src/launcher-item.c b/src/launcher-item.c new file mode 100644 index 0000000..c4de70b --- /dev/null +++ b/src/launcher-item.c @@ -0,0 +1,73 @@ +#include "launcher-item.h" + +static void foobar_launcher_item_default_init( FoobarLauncherItemInterface* iface ); + +G_DEFINE_INTERFACE( FoobarLauncherItem, foobar_launcher_item, G_TYPE_OBJECT ) + +// +// Static initialization for the launcher item interface. +// +void foobar_launcher_item_default_init( FoobarLauncherItemInterface* iface ) +{ + g_object_interface_install_property( iface, g_param_spec_string( + "title", + "Title", + "The title text displayed in the launcher.", + NULL, + G_PARAM_READABLE ) ); + g_object_interface_install_property( iface, g_param_spec_string( + "description", + "Description", + "Text displayed below the title.", + NULL, + G_PARAM_READABLE ) ); + g_object_interface_install_property( iface, g_param_spec_object( + "icon", + "Icon", + "Icon to show for the item.", + G_TYPE_ICON, + G_PARAM_READABLE ) ); +} + +// +// Get the title text displayed in the launcher. +// +gchar const* foobar_launcher_item_get_title( FoobarLauncherItem* self ) +{ + g_return_val_if_fail( FOOBAR_IS_LAUNCHER_ITEM( self ), NULL ); + FoobarLauncherItemInterface* iface = FOOBAR_LAUNCHER_ITEM_GET_IFACE( self ); + g_return_val_if_fail( iface->get_title != NULL, NULL ); + return iface->get_title( self ); +} + +// +// Get the text displayed below the title. +// +gchar const* foobar_launcher_item_get_description( FoobarLauncherItem* self ) +{ + g_return_val_if_fail( FOOBAR_IS_LAUNCHER_ITEM( self ), NULL ); + FoobarLauncherItemInterface* iface = FOOBAR_LAUNCHER_ITEM_GET_IFACE( self ); + g_return_val_if_fail( iface->get_description != NULL, NULL ); + return iface->get_description( self ); +} + +// +// Get the icon to show for the item. +// +GIcon* foobar_launcher_item_get_icon( FoobarLauncherItem* self ) +{ + g_return_val_if_fail( FOOBAR_IS_LAUNCHER_ITEM( self ), NULL ); + FoobarLauncherItemInterface* iface = FOOBAR_LAUNCHER_ITEM_GET_IFACE( self ); + g_return_val_if_fail( iface->get_icon != NULL, NULL ); + return iface->get_icon( self ); +} + +// +// Activate the item, e.g. by launching the application. +// +void foobar_launcher_item_activate( FoobarLauncherItem* self ) +{ + g_return_if_fail( FOOBAR_IS_LAUNCHER_ITEM( self ) ); + FoobarLauncherItemInterface* iface = FOOBAR_LAUNCHER_ITEM_GET_IFACE( self ); + if ( iface->activate ) { iface->activate( self ); } +} diff --git a/src/launcher-item.h b/src/launcher-item.h new file mode 100644 index 0000000..b2d274a --- /dev/null +++ b/src/launcher-item.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_LAUNCHER_ITEM foobar_launcher_item_get_type( ) + +G_DECLARE_INTERFACE( FoobarLauncherItem, foobar_launcher_item, FOOBAR, LAUNCHER_ITEM, GObject ) + +struct _FoobarLauncherItemInterface +{ + GTypeInterface parent_interface; + + gchar const* ( *get_title ) ( FoobarLauncherItem* self ); + gchar const* ( *get_description )( FoobarLauncherItem* self ); + GIcon* ( *get_icon ) ( FoobarLauncherItem* self ); + void ( *activate ) ( FoobarLauncherItem* self ); +}; + +gchar const* foobar_launcher_item_get_title ( FoobarLauncherItem* self ); +gchar const* foobar_launcher_item_get_description( FoobarLauncherItem* self ); +GIcon* foobar_launcher_item_get_icon ( FoobarLauncherItem* self ); +void foobar_launcher_item_activate ( FoobarLauncherItem* self ); + +G_END_DECLS + diff --git a/src/launcher.c b/src/launcher.c index da37715..f4b34d8 100644 --- a/src/launcher.c +++ b/src/launcher.c @@ -1,4 +1,5 @@ #include "launcher.h" +#include "launcher-item.h" #include "widgets/limit-container.h" #include #include @@ -21,8 +22,11 @@ struct _FoobarLauncher GtkWidget* list_view; GtkWidget* limit_container; GtkFilterListModel* filter_model; + GListStore* quick_answer_model; + GtkFlattenListModel* flatten_model; GtkSingleSelection* selection_model; FoobarApplicationService* application_service; + FoobarQuickAnswerService* quick_answer_service; FoobarConfigurationService* configuration_service; gulong config_handler_id; }; @@ -119,9 +123,28 @@ void foobar_launcher_init( FoobarLauncher* self ) GtkWidget *separator = gtk_separator_new( GTK_ORIENTATION_HORIZONTAL ); + // Set up the results list view and an event controller for auto-switching focus to the input. + + GtkListItemFactory* item_factory = gtk_signal_list_item_factory_new( ); + g_signal_connect( item_factory, "setup", G_CALLBACK( foobar_launcher_handle_item_setup ), NULL ); + + GtkCustomFilter* filter = gtk_custom_filter_new( foobar_launcher_filter_func, self, NULL ); + + self->filter_model = gtk_filter_list_model_new( NULL, GTK_FILTER( filter ) ); + + self->quick_answer_model = g_list_store_new( FOOBAR_TYPE_LAUNCHER_ITEM ); + + GListStore* flattened_models = g_list_store_new( G_TYPE_LIST_MODEL ); + g_list_store_append( flattened_models, self->quick_answer_model ); + g_list_store_append( flattened_models, self->filter_model ); + + self->flatten_model = gtk_flatten_list_model_new( G_LIST_MODEL( flattened_models ) ); + + self->selection_model = gtk_single_selection_new( G_LIST_MODEL( g_object_ref( self->flatten_model ) ) ); + { - GtkExpression* list_expr = gtk_constant_expression_new( GTK_TYPE_FILTER_LIST_MODEL, self->filter_model ); - GtkExpression* count_expr = gtk_property_expression_new( GTK_TYPE_FILTER_LIST_MODEL, list_expr, "n-items" ); + GtkExpression* list_expr = gtk_constant_expression_new( GTK_TYPE_FLATTEN_LIST_MODEL, self->flatten_model ); + GtkExpression* count_expr = gtk_property_expression_new( GTK_TYPE_FLATTEN_LIST_MODEL, list_expr, "n-items" ); GtkExpression* visible_params[] = { count_expr }; GtkExpression* visible_expr = gtk_cclosure_expression_new( G_TYPE_BOOLEAN, @@ -134,17 +157,6 @@ void foobar_launcher_init( FoobarLauncher* self ) gtk_expression_bind( visible_expr, separator, "visible", NULL ); } - // Set up the results list view and an event controller for auto-switching focus to the input. - - GtkListItemFactory* item_factory = gtk_signal_list_item_factory_new( ); - g_signal_connect( item_factory, "setup", G_CALLBACK( foobar_launcher_handle_item_setup ), NULL ); - - GtkCustomFilter* filter = gtk_custom_filter_new( foobar_launcher_filter_func, self, NULL ); - - self->filter_model = gtk_filter_list_model_new( NULL, GTK_FILTER( filter ) ); - - self->selection_model = gtk_single_selection_new( G_LIST_MODEL( g_object_ref( self->filter_model ) ) ); - GtkEventController* list_controller = gtk_event_controller_key_new( ); g_signal_connect( list_controller, "key-pressed", G_CALLBACK( foobar_launcher_handle_list_key ), self ); g_signal_connect( list_controller, "key-released", G_CALLBACK( foobar_launcher_handle_list_key ), self ); @@ -201,8 +213,11 @@ void foobar_launcher_finalize( GObject* object ) g_clear_signal_handler( &self->config_handler_id, self->configuration_service ); g_clear_object( &self->filter_model ); + g_clear_object( &self->quick_answer_model ); + g_clear_object( &self->flatten_model ); g_clear_object( &self->selection_model ); g_clear_object( &self->application_service ); + g_clear_object( &self->quick_answer_service ); g_clear_object( &self->configuration_service ); g_clear_pointer( &self->search_terms, g_strfreev ); @@ -218,13 +233,16 @@ void foobar_launcher_finalize( GObject* object ) // FoobarLauncher* foobar_launcher_new( FoobarApplicationService* application_service, + FoobarQuickAnswerService* quick_answer_service, FoobarConfigurationService* configuration_service ) { g_return_val_if_fail( FOOBAR_IS_APPLICATION_SERVICE( application_service ), NULL ); + g_return_val_if_fail( FOOBAR_IS_QUICK_ANSWER_SERVICE( quick_answer_service ), NULL ); g_return_val_if_fail( FOOBAR_IS_CONFIGURATION_SERVICE( configuration_service ), NULL ); FoobarLauncher* self = g_object_new( FOOBAR_TYPE_LAUNCHER, NULL ); self->application_service = g_object_ref( application_service ); + self->quick_answer_service = g_object_ref( quick_answer_service ); self->configuration_service = g_object_ref( configuration_service ); // Set up the result list view's source model. @@ -299,6 +317,17 @@ void foobar_launcher_handle_search_changed( { FoobarLauncher* self = (FoobarLauncher*)userdata; + // Update quick answer. + + g_autoptr(FoobarQuickAnswer) answer = foobar_quick_answer_service_query( self->quick_answer_service, gtk_editable_get_text( editable ) ); + g_list_store_remove_all( self->quick_answer_model ); + if (answer) + { + g_list_store_append(self->quick_answer_model, answer); + } + + // Update filter. + GStrvBuilder* terms_builder = g_strv_builder_new( ); g_autofree gchar* query = g_strdup( gtk_editable_get_text( editable ) ); gchar* query_start = query; @@ -416,24 +445,24 @@ void foobar_launcher_handle_item_setup( gtk_widget_set_valign( icon, GTK_ALIGN_CENTER ); gtk_widget_add_css_class( icon, "icon" ); - GtkWidget* name = gtk_label_new( NULL ); - gtk_label_set_justify( GTK_LABEL( name ), GTK_JUSTIFY_LEFT ); - gtk_label_set_xalign( GTK_LABEL( name ), 0 ); - gtk_label_set_wrap( GTK_LABEL( name ), FALSE ); - gtk_label_set_ellipsize( GTK_LABEL( name ), PANGO_ELLIPSIZE_END ); - gtk_widget_add_css_class( name, "name" ); + GtkWidget* title = gtk_label_new( NULL ); + gtk_label_set_justify( GTK_LABEL( title ), GTK_JUSTIFY_LEFT ); + gtk_label_set_xalign( GTK_LABEL( title ), 0 ); + gtk_label_set_wrap( GTK_LABEL( title ), FALSE ); + gtk_label_set_ellipsize( GTK_LABEL( title ), PANGO_ELLIPSIZE_END ); + gtk_widget_add_css_class( title, "name" ); GtkWidget* description = gtk_label_new( NULL ); gtk_label_set_justify( GTK_LABEL( description ), GTK_JUSTIFY_LEFT ); gtk_label_set_xalign( GTK_LABEL( description ), 0 ); - gtk_label_set_wrap( GTK_LABEL( name ), FALSE ); + gtk_label_set_wrap( GTK_LABEL( description ), FALSE ); gtk_label_set_ellipsize( GTK_LABEL( description ), PANGO_ELLIPSIZE_END ); gtk_widget_add_css_class( description, "description" ); GtkWidget* column = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 ); gtk_widget_set_hexpand( column, TRUE ); gtk_widget_set_valign( column, GTK_ALIGN_CENTER ); - gtk_box_append( GTK_BOX( column ), name ); + gtk_box_append( GTK_BOX( column ), title ); gtk_box_append( GTK_BOX( column ), description ); GtkWidget* row = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 ); @@ -447,7 +476,7 @@ void foobar_launcher_handle_item_setup( { GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" ); - GtkExpression* icon_expr = gtk_property_expression_new( FOOBAR_TYPE_APPLICATION_ITEM, item_expr, "icon" ); + GtkExpression* icon_expr = gtk_property_expression_new( FOOBAR_TYPE_LAUNCHER_ITEM, item_expr, "icon" ); gtk_expression_bind( gtk_expression_ref( icon_expr ), icon, "gicon", list_item ); GtkExpression* visible_params[] = { icon_expr }; GtkExpression* visible_expr = gtk_cclosure_expression_new( @@ -463,13 +492,13 @@ void foobar_launcher_handle_item_setup( { GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" ); - GtkExpression* name_expr = gtk_property_expression_new( FOOBAR_TYPE_APPLICATION_ITEM, item_expr, "name" ); - gtk_expression_bind( name_expr, name, "label", list_item ); + GtkExpression* title_expr = gtk_property_expression_new( FOOBAR_TYPE_LAUNCHER_ITEM, item_expr, "title" ); + gtk_expression_bind( title_expr, title, "label", list_item ); } { GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" ); - GtkExpression* description_expr = gtk_property_expression_new( FOOBAR_TYPE_APPLICATION_ITEM, item_expr, "description" ); + GtkExpression* description_expr = gtk_property_expression_new( FOOBAR_TYPE_LAUNCHER_ITEM, item_expr, "description" ); gtk_expression_bind( gtk_expression_ref( description_expr ), description, "label", list_item ); GtkExpression* visible_params[] = { description_expr }; GtkExpression* visible_expr = gtk_cclosure_expression_new( @@ -496,8 +525,8 @@ void foobar_launcher_handle_item_activate( FoobarLauncher* self = (FoobarLauncher*)userdata; GListModel* list = G_LIST_MODEL( gtk_list_view_get_model( view ) ); - FoobarApplicationItem* item = g_list_model_get_item( list, position ); - foobar_application_item_launch( item ); + FoobarLauncherItem* item = g_list_model_get_item( list, position ); + foobar_launcher_item_activate( item ); gtk_widget_set_visible( GTK_WIDGET( self ), FALSE ); } diff --git a/src/launcher.h b/src/launcher.h index 4c3e495..5d92607 100644 --- a/src/launcher.h +++ b/src/launcher.h @@ -3,6 +3,7 @@ #include #include "services/application-service.h" #include "services/configuration-service.h" +#include "services/quick-answer-service.h" G_BEGIN_DECLS @@ -11,6 +12,7 @@ G_BEGIN_DECLS G_DECLARE_FINAL_TYPE( FoobarLauncher, foobar_launcher, FOOBAR, LAUNCHER, GtkWindow ) FoobarLauncher* foobar_launcher_new ( FoobarApplicationService* application_service, + FoobarQuickAnswerService* quick_answer_service, FoobarConfigurationService* configuration_service ); void foobar_launcher_apply_configuration( FoobarLauncher* self, FoobarLauncherConfiguration const* config ); diff --git a/src/meson.build b/src/meson.build index b089bce..78e0575 100644 --- a/src/meson.build +++ b/src/meson.build @@ -4,6 +4,7 @@ foobar_sources = files( 'utils.c', 'panel.c', 'launcher.c', + 'launcher-item.c', 'control-center.c', 'notification-area.c', ) diff --git a/src/services/application-service.c b/src/services/application-service.c index 3885719..42835ac 100644 --- a/src/services/application-service.c +++ b/src/services/application-service.c @@ -1,4 +1,5 @@ #include "services/application-service.h" +#include "launcher-item.h" #include "utils.h" #include #include @@ -21,29 +22,38 @@ struct _FoobarApplicationItem enum { APP_PROP_ID = 1, - APP_PROP_NAME, - APP_PROP_DESCRIPTION, APP_PROP_EXECUTABLE, APP_PROP_CATEGORIES, - APP_PROP_ICON, APP_PROP_FREQUENCY, + APP_PROP_TITLE, + APP_PROP_DESCRIPTION, + APP_PROP_ICON, N_APP_PROPS, }; static GParamSpec* app_props[N_APP_PROPS] = { 0 }; -static void foobar_application_item_class_init ( FoobarApplicationItemClass* klass ); -static void foobar_application_item_init ( FoobarApplicationItem* self ); -static void foobar_application_item_get_property( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_application_item_finalize ( GObject* object ); -static FoobarApplicationItem* foobar_application_item_new ( FoobarApplicationService* service ); -static void foobar_application_item_set_info ( FoobarApplicationItem* self, - GAppInfo* value ); - -G_DEFINE_FINAL_TYPE( FoobarApplicationItem, foobar_application_item, G_TYPE_OBJECT ) +static void foobar_application_item_class_init ( FoobarApplicationItemClass* klass ); +static void foobar_application_item_launcher_item_interface_init( FoobarLauncherItemInterface* iface ); +static void foobar_application_item_init ( FoobarApplicationItem* self ); +static void foobar_application_item_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_application_item_finalize ( GObject* object ); +static FoobarApplicationItem* foobar_application_item_new ( FoobarApplicationService* service ); +static gchar const* foobar_application_item_get_title ( FoobarLauncherItem* self ); +static gchar const* foobar_application_item_get_description ( FoobarLauncherItem* self ); +static GIcon* foobar_application_item_get_icon ( FoobarLauncherItem* self ); +static void foobar_application_item_activate ( FoobarLauncherItem* self ); +static void foobar_application_item_set_info ( FoobarApplicationItem* self, + GAppInfo* value ); + +G_DEFINE_FINAL_TYPE_WITH_CODE( + FoobarApplicationItem, + foobar_application_item, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE( FOOBAR_TYPE_LAUNCHER_ITEM, foobar_application_item_launcher_item_interface_init ) ) // // FoobarApplicationService: @@ -124,24 +134,13 @@ void foobar_application_item_class_init( FoobarApplicationItemClass* klass ) object_klass->get_property = foobar_application_item_get_property; object_klass->finalize = foobar_application_item_finalize; + gpointer launcher_item_iface = g_type_default_interface_peek( FOOBAR_TYPE_LAUNCHER_ITEM ); app_props[APP_PROP_ID] = g_param_spec_string( "id", "ID", "Application identifier string.", NULL, G_PARAM_READABLE ); - app_props[APP_PROP_NAME] = g_param_spec_string( - "name", - "Name", - "Displayed name for the application.", - NULL, - G_PARAM_READABLE ); - app_props[APP_PROP_DESCRIPTION] = g_param_spec_string( - "description", - "Description", - "Brief description of the application.", - NULL, - G_PARAM_READABLE ); app_props[APP_PROP_EXECUTABLE] = g_param_spec_string( "executable", "Executable", @@ -154,12 +153,6 @@ void foobar_application_item_class_init( FoobarApplicationItemClass* klass ) "Semicolon-separated list of application categories.", NULL, G_PARAM_READABLE ); - app_props[APP_PROP_ICON] = g_param_spec_object( - "icon", - "Icon", - "Icon to show for the application.", - G_TYPE_ICON, - G_PARAM_READABLE ); app_props[APP_PROP_FREQUENCY] = g_param_spec_int64( "frequency", "Frequency", @@ -168,9 +161,29 @@ void foobar_application_item_class_init( FoobarApplicationItemClass* klass ) INT64_MAX, 0, G_PARAM_READABLE ); + app_props[APP_PROP_TITLE] = g_param_spec_override( + "title", + g_object_interface_find_property( launcher_item_iface, "title" ) ); + app_props[APP_PROP_DESCRIPTION] = g_param_spec_override( + "description", + g_object_interface_find_property( launcher_item_iface, "description" ) ); + app_props[APP_PROP_ICON] = g_param_spec_override( + "icon", + g_object_interface_find_property( launcher_item_iface, "icon" ) ); g_object_class_install_properties( object_klass, N_APP_PROPS, app_props ); } +// +// Static initialization of the FoobarLauncherItem interface. +// +void foobar_application_item_launcher_item_interface_init( FoobarLauncherItemInterface* iface ) +{ + iface->get_title = foobar_application_item_get_title; + iface->get_description = foobar_application_item_get_description; + iface->get_icon = foobar_application_item_get_icon; + iface->activate = foobar_application_item_activate; +} + // // Instance initialization for application items. // @@ -195,24 +208,24 @@ void foobar_application_item_get_property( case APP_PROP_ID: g_value_set_string( value, foobar_application_item_get_id( self ) ); break; - case APP_PROP_NAME: - g_value_set_string( value, foobar_application_item_get_name( self ) ); - break; - case APP_PROP_DESCRIPTION: - g_value_set_string( value, foobar_application_item_get_description( self ) ); - break; case APP_PROP_EXECUTABLE: g_value_set_string( value, foobar_application_item_get_executable( self ) ); break; case APP_PROP_CATEGORIES: g_value_set_string( value, foobar_application_item_get_categories( self ) ); break; - case APP_PROP_ICON: - g_value_set_object( value, foobar_application_item_get_icon( self ) ); - break; case APP_PROP_FREQUENCY: g_value_set_int64( value, foobar_application_item_get_frequency( self ) ); break; + case APP_PROP_TITLE: + g_value_set_string( value, foobar_launcher_item_get_title( FOOBAR_LAUNCHER_ITEM ( self ) ) ); + break; + case APP_PROP_DESCRIPTION: + g_value_set_string( value, foobar_launcher_item_get_description( FOOBAR_LAUNCHER_ITEM( self ) ) ); + break; + case APP_PROP_ICON: + g_value_set_object( value, foobar_launcher_item_get_icon( FOOBAR_LAUNCHER_ITEM( self ) ) ); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); break; @@ -251,62 +264,91 @@ gchar const* foobar_application_item_get_id( FoobarApplicationItem* self ) } // -// Get the application's human-readable name. +// Get the application's executable name. // -gchar const* foobar_application_item_get_name( FoobarApplicationItem* self ) +gchar const* foobar_application_item_get_executable( FoobarApplicationItem* self ) { g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), NULL ); - return self->info ? g_app_info_get_display_name( self->info ) : NULL; + return self->info ? g_app_info_get_executable( self->info ) : NULL; } // -// Get an optional short description for the application. +// Get the semicolon-separated list of categories for this application. // -gchar const* foobar_application_item_get_description( FoobarApplicationItem* self ) +gchar const* foobar_application_item_get_categories( FoobarApplicationItem* self ) { g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), NULL ); - return self->info ? g_app_info_get_description( self->info ) : NULL; + return G_IS_DESKTOP_APP_INFO( self->info ) + ? g_desktop_app_info_get_categories( G_DESKTOP_APP_INFO( self->info ) ) + : NULL; } // -// Get the application's executable name. +// Get the number of times this application was opened using the launcher. // -gchar const* foobar_application_item_get_executable( FoobarApplicationItem* self ) +gint64 foobar_application_item_get_frequency( FoobarApplicationItem* self ) { - g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), NULL ); - return self->info ? g_app_info_get_executable( self->info ) : NULL; + g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), 0 ); + + if ( !self->service ) { return 0; } + gpointer value = g_hash_table_lookup( self->service->frequencies, foobar_application_item_get_id( self ) ); + return GPOINTER_TO_SIZE( value ); } // -// Get the semicolon-separated list of categories for this application. +// Get the application's human-readable name. // -gchar const* foobar_application_item_get_categories( FoobarApplicationItem* self ) +gchar const* foobar_application_item_get_title( FoobarLauncherItem* item ) { - g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), NULL ); - return G_IS_DESKTOP_APP_INFO( self->info ) - ? g_desktop_app_info_get_categories( G_DESKTOP_APP_INFO( self->info ) ) - : NULL; + FoobarApplicationItem* self = ( FoobarApplicationItem* )item; + return self->info ? g_app_info_get_display_name( self->info ) : NULL; +} + +// +// Get an optional short description for the application. +// +gchar const* foobar_application_item_get_description( FoobarLauncherItem* item ) +{ + FoobarApplicationItem* self = ( FoobarApplicationItem* )item; + return self->info ? g_app_info_get_description( self->info ) : NULL; } // // Get an icon representing the application. // -GIcon* foobar_application_item_get_icon( FoobarApplicationItem* self ) +GIcon* foobar_application_item_get_icon( FoobarLauncherItem* item ) { - g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), NULL ); + FoobarApplicationItem* self = ( FoobarApplicationItem* )item; return self->info ? g_app_info_get_icon( self->info ) : NULL; } // -// Get the number of times this application was opened using the launcher. +// Launch the application, increasing its frequency by one. // -gint64 foobar_application_item_get_frequency( FoobarApplicationItem* self ) +void foobar_application_item_activate( FoobarLauncherItem* item ) { - g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), 0 ); + FoobarApplicationItem* self = ( FoobarApplicationItem* )item; - if ( !self->service ) { return 0; } - gpointer value = g_hash_table_lookup( self->service->frequencies, foobar_application_item_get_id( self ) ); - return GPOINTER_TO_SIZE( value ); + if ( !self->info ) { return; } + + g_autoptr( GError ) error = NULL; + if ( !g_app_info_launch( self->info, NULL, NULL, &error ) ) + { + g_warning( "Unable to launch application: %s", error->message ); + } + + gchar const* id = foobar_application_item_get_id( self ); + if ( !self->service || !id ) { return; } + + gpointer value = g_hash_table_lookup( self->service->frequencies, id ); + value = GSIZE_TO_POINTER( GPOINTER_TO_SIZE( value ) + 1 ); + g_mutex_lock( &self->service->frequencies_mutex ); + g_hash_table_insert( self->service->frequencies, g_strdup( id ), value ); + g_mutex_unlock( &self->service->frequencies_mutex ); + + g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_FREQUENCY] ); + gtk_sorter_changed( gtk_sort_list_model_get_sorter( self->service->sorted_items ), GTK_SORTER_CHANGE_DIFFERENT ); + foobar_application_service_write_cache( self->service ); } // @@ -325,7 +367,7 @@ void foobar_application_item_set_info( if ( value ) { self->info = g_object_ref( value ); } g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_ID] ); - g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_NAME] ); + g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_TITLE] ); g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_DESCRIPTION] ); g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_EXECUTABLE] ); g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_ICON] ); @@ -346,8 +388,8 @@ gboolean foobar_application_item_match( for ( gchar const* const* it = terms; *it; ++it ) { gchar const* term = *it; - gchar const* name = foobar_application_item_get_name( self ); - gchar const* description = foobar_application_item_get_description( self ); + gchar const* name = foobar_launcher_item_get_title( FOOBAR_LAUNCHER_ITEM( self ) ); + gchar const* description = foobar_launcher_item_get_description( FOOBAR_LAUNCHER_ITEM( self ) ); gchar const* executable = foobar_application_item_get_executable( self ); gchar const* categories = foobar_application_item_get_categories( self ); gchar const* id = foobar_application_item_get_id( self ); @@ -363,35 +405,6 @@ gboolean foobar_application_item_match( return TRUE; } -// -// Launch the application, increasing its frequency by one. -// -void foobar_application_item_launch( FoobarApplicationItem* self ) -{ - g_return_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ) ); - - if ( !self->info ) { return; } - - g_autoptr( GError ) error = NULL; - if ( !g_app_info_launch( self->info, NULL, NULL, &error ) ) - { - g_warning( "Unable to launch application: %s", error->message ); - } - - gchar const* id = foobar_application_item_get_id( self ); - if ( !self->service || !id ) { return; } - - gpointer value = g_hash_table_lookup( self->service->frequencies, id ); - value = GSIZE_TO_POINTER( GPOINTER_TO_SIZE( value ) + 1 ); - g_mutex_lock( &self->service->frequencies_mutex ); - g_hash_table_insert( self->service->frequencies, g_strdup( id ), value ); - g_mutex_unlock( &self->service->frequencies_mutex ); - - g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_FREQUENCY] ); - gtk_sorter_changed( gtk_sort_list_model_get_sorter( self->service->sorted_items ), GTK_SORTER_CHANGE_DIFFERENT ); - foobar_application_service_write_cache( self->service ); -} - // --------------------------------------------------------------------------------------------------------------------- // Service Implementation // --------------------------------------------------------------------------------------------------------------------- @@ -722,8 +735,8 @@ gint foobar_application_service_sort_func( if ( freq_a > freq_b ) { return -1; } if ( freq_a < freq_b ) { return 1; } - gchar const* name_a = foobar_application_item_get_name( app_a ); - gchar const* name_b = foobar_application_item_get_name( app_b ); + gchar const* name_a = foobar_launcher_item_get_title( FOOBAR_LAUNCHER_ITEM( app_a ) ); + gchar const* name_b = foobar_launcher_item_get_title( FOOBAR_LAUNCHER_ITEM( app_b ) ); gint name_res = g_strcmp0( name_a, name_b ); if ( name_res ) { return name_res; } diff --git a/src/services/application-service.h b/src/services/application-service.h index 3c2622a..dd043fc 100644 --- a/src/services/application-service.h +++ b/src/services/application-service.h @@ -11,15 +11,11 @@ G_BEGIN_DECLS G_DECLARE_FINAL_TYPE( FoobarApplicationItem, foobar_application_item, FOOBAR, APPLICATION_ITEM, GObject ) gchar const* foobar_application_item_get_id ( FoobarApplicationItem* self ); -gchar const* foobar_application_item_get_name ( FoobarApplicationItem* self ); -gchar const* foobar_application_item_get_description( FoobarApplicationItem* self ); gchar const* foobar_application_item_get_executable ( FoobarApplicationItem* self ); gchar const* foobar_application_item_get_categories ( FoobarApplicationItem* self ); -GIcon* foobar_application_item_get_icon ( FoobarApplicationItem* self ); gint64 foobar_application_item_get_frequency ( FoobarApplicationItem* self ); gboolean foobar_application_item_match ( FoobarApplicationItem* self, gchar const* const* terms ); -void foobar_application_item_launch ( FoobarApplicationItem* self ); G_DECLARE_FINAL_TYPE( FoobarApplicationService, foobar_application_service, FOOBAR, APPLICATION_SERVICE, GObject ) diff --git a/src/services/meson.build b/src/services/meson.build index 990102b..e4f151d 100644 --- a/src/services/meson.build +++ b/src/services/meson.build @@ -8,5 +8,6 @@ foobar_sources += files( 'network-service.c', 'bluetooth-service.c', 'application-service.c', + 'quick-answer-service.c', 'configuration-service.c', ) diff --git a/src/services/quick-answer-service.c b/src/services/quick-answer-service.c new file mode 100644 index 0000000..b564a16 --- /dev/null +++ b/src/services/quick-answer-service.c @@ -0,0 +1,317 @@ +#include "services/quick-answer-service.h" +#include "launcher-item.h" +#include + +struct _FoobarQuickAnswer +{ + GObject parent_instance; + gchar* title; + gchar* value; + GIcon* icon; +}; + +enum +{ + ANSWER_PROP_VALUE = 1, + ANSWER_PROP_TITLE, + ANSWER_PROP_DESCRIPTION, + ANSWER_PROP_ICON, + N_ANSWER_PROPS, +}; + +static GParamSpec* answer_props[N_ANSWER_PROPS] = { 0 }; + +static void foobar_quick_answer_class_init ( FoobarQuickAnswerClass* klass ); +static void foobar_quick_answer_launcher_item_interface_init( FoobarLauncherItemInterface* iface ); +static void foobar_quick_answer_init ( FoobarQuickAnswer* self ); +static void foobar_quick_answer_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_quick_answer_finalize ( GObject* object ); +static FoobarQuickAnswer* foobar_quick_answer_new ( void ); +static gchar const* foobar_quick_answer_get_title ( FoobarLauncherItem* self ); +static gchar const* foobar_quick_answer_get_description ( FoobarLauncherItem* self ); +static GIcon* foobar_quick_answer_get_icon ( FoobarLauncherItem* self ); +static void foobar_quick_answer_activate ( FoobarLauncherItem* self ); +static void foobar_quick_answer_set_title ( FoobarQuickAnswer* self, + gchar const* value ); +static void foobar_quick_answer_set_value ( FoobarQuickAnswer* self, + gchar const* value ); +static void foobar_quick_answer_set_icon ( FoobarQuickAnswer* self, + GIcon* value ); + +G_DEFINE_FINAL_TYPE_WITH_CODE( + FoobarQuickAnswer, + foobar_quick_answer, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE( FOOBAR_TYPE_LAUNCHER_ITEM, foobar_quick_answer_launcher_item_interface_init ) ) + +struct _FoobarQuickAnswerService +{ + GObject parent_instance; +}; + +static void foobar_quick_answer_service_class_init( FoobarQuickAnswerServiceClass* klass ); +static void foobar_quick_answer_service_init ( FoobarQuickAnswerService* self ); + +G_DEFINE_FINAL_TYPE( FoobarQuickAnswerService, foobar_quick_answer_service, G_TYPE_OBJECT ) + +// --------------------------------------------------------------------------------------------------------------------- +// Quick Answers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for quick answers. +// +void foobar_quick_answer_class_init( FoobarQuickAnswerClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_quick_answer_get_property; + object_klass->finalize = foobar_quick_answer_finalize; + + gpointer launcher_item_iface = g_type_default_interface_peek( FOOBAR_TYPE_LAUNCHER_ITEM ); + answer_props[ANSWER_PROP_VALUE] = g_param_spec_string( + "value", + "Value", + "The raw value of the quick answer.", + NULL, + G_PARAM_READABLE ); + answer_props[ANSWER_PROP_TITLE] = g_param_spec_override( + "title", + g_object_interface_find_property( launcher_item_iface, "title" ) ); + answer_props[ANSWER_PROP_DESCRIPTION] = g_param_spec_override( + "description", + g_object_interface_find_property( launcher_item_iface, "description" ) ); + answer_props[ANSWER_PROP_ICON] = g_param_spec_override( + "icon", + g_object_interface_find_property( launcher_item_iface, "icon" ) ); + g_object_class_install_properties( object_klass, N_ANSWER_PROPS, answer_props ); +} + +// +// Static initialization of the FoobarLauncherItem interface. +// +void foobar_quick_answer_launcher_item_interface_init( FoobarLauncherItemInterface* iface ) +{ + iface->get_title = foobar_quick_answer_get_title; + iface->get_description = foobar_quick_answer_get_description; + iface->get_icon = foobar_quick_answer_get_icon; + iface->activate = foobar_quick_answer_activate; +} + +// +// Instance initialization for quick answers. +// +void foobar_quick_answer_init( FoobarQuickAnswer* self ) +{ + (void)self; +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_quick_answer_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarQuickAnswer* self = (FoobarQuickAnswer*)object; + + switch ( prop_id ) + { + case ANSWER_PROP_VALUE: + g_value_set_string( value, foobar_quick_answer_get_value( self ) ); + break; + case ANSWER_PROP_TITLE: + g_value_set_string( value, foobar_launcher_item_get_title( FOOBAR_LAUNCHER_ITEM ( self ) ) ); + break; + case ANSWER_PROP_DESCRIPTION: + g_value_set_string( value, foobar_launcher_item_get_description( FOOBAR_LAUNCHER_ITEM( self ) ) ); + break; + case ANSWER_PROP_ICON: + g_value_set_object( value, foobar_launcher_item_get_icon( FOOBAR_LAUNCHER_ITEM( self ) ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for quick answers. +// +void foobar_quick_answer_finalize( GObject* object ) +{ + FoobarQuickAnswer* self = (FoobarQuickAnswer*)object; + + g_clear_pointer( &self->title, g_free ); + g_clear_pointer( &self->value, g_free ); + g_clear_object( &self->icon ); + + G_OBJECT_CLASS( foobar_quick_answer_parent_class )->finalize( object ); +} + +// +// Create a new, default-initialized quick answer. +// +FoobarQuickAnswer* foobar_quick_answer_new( void ) +{ + return g_object_new( FOOBAR_TYPE_QUICK_ANSWER, NULL ); +} + +// +// Get the raw value of the quick answer. +// +gchar const* foobar_quick_answer_get_value( FoobarQuickAnswer* self ) +{ + g_return_val_if_fail( FOOBAR_IS_QUICK_ANSWER( self ), NULL ); + return self->value; +} + +// +// Get the title for the quick answer. +// +gchar const* foobar_quick_answer_get_title( FoobarLauncherItem* item ) +{ + FoobarQuickAnswer* self = ( FoobarQuickAnswer* )item; + return self->title; +} + +// +// Get the description shown below the title, always NULL for quick answers. +// +gchar const* foobar_quick_answer_get_description( FoobarLauncherItem* item ) +{ + (void)item; + return NULL; +} + +// +// Get an icon representing the quick answer. +// +GIcon* foobar_quick_answer_get_icon( FoobarLauncherItem* item ) +{ + FoobarQuickAnswer* self = ( FoobarQuickAnswer* )item; + return self->icon; +} + +// +// Copy the quick answer's value into the clipboard. +// +void foobar_quick_answer_activate( FoobarLauncherItem* item ) +{ + FoobarQuickAnswer* self = ( FoobarQuickAnswer* )item; + if ( self->value ) + { + GdkClipboard* clipboard = gdk_display_get_clipboard( gdk_display_get_default( ) ); + gdk_clipboard_set_text( clipboard, self->value ); + } +} + +// +// Update the raw value of the quick answer. +// +void foobar_quick_answer_set_value( + FoobarQuickAnswer* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_QUICK_ANSWER( self ) ); + + if ( g_strcmp0( self->value, value ) ) + { + g_clear_pointer( &self->value, g_free ); + self->value = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), answer_props[ANSWER_PROP_VALUE] ); + } +} + +// +// Update the title for the quick answer. +// +void foobar_quick_answer_set_title( + FoobarQuickAnswer* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_QUICK_ANSWER( self ) ); + + if ( g_strcmp0( self->title, value ) ) + { + g_clear_pointer( &self->title, g_free ); + self->title = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), answer_props[ANSWER_PROP_TITLE] ); + } +} + +// +// Update the icon representing the quick answer. +// +void foobar_quick_answer_set_icon( + FoobarQuickAnswer* self, + GIcon* value ) +{ + g_return_if_fail( FOOBAR_IS_QUICK_ANSWER( self ) ); + + if ( self->icon != value ) + { + g_clear_object( &self->icon ); + self->icon = value ? g_object_ref( value ) : NULL; + g_object_notify_by_pspec( G_OBJECT( self ), answer_props[ANSWER_PROP_ICON] ); + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Service Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for the quick answer service. +// +void foobar_quick_answer_service_class_init( FoobarQuickAnswerServiceClass* klass ) +{ + (void)klass; +} + +// +// Instance initialization for the quick answer service. +// +void foobar_quick_answer_service_init( FoobarQuickAnswerService* self ) +{ + (void)self; +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new quick answer service instance. +// +FoobarQuickAnswerService* foobar_quick_answer_service_new( void ) +{ + return g_object_new( FOOBAR_TYPE_QUICK_ANSWER_SERVICE, NULL ); +} + +// +// Query the service for a quick answer. +// +FoobarQuickAnswer* foobar_quick_answer_service_query( + FoobarQuickAnswerService* self, + gchar const* query ) +{ + (void)self; + + if ( !g_strcmp0(query, "2+2") ) + { + g_autoptr(GIcon) icon = g_themed_icon_new("fluent-calculator-symbolic"); + + FoobarQuickAnswer* answer = foobar_quick_answer_new(); + foobar_quick_answer_set_value(answer, "4"); + foobar_quick_answer_set_title(answer, "= 4"); + foobar_quick_answer_set_icon(answer, icon); + return answer; + } + + return NULL; +} diff --git a/src/services/quick-answer-service.h b/src/services/quick-answer-service.h new file mode 100644 index 0000000..02a8e10 --- /dev/null +++ b/src/services/quick-answer-service.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_QUICK_ANSWER foobar_quick_answer_get_type( ) +#define FOOBAR_TYPE_QUICK_ANSWER_SERVICE foobar_quick_answer_service_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarQuickAnswer, foobar_quick_answer, FOOBAR, QUICK_ANSWER, GObject ) + +gchar const* foobar_quick_answer_get_value( FoobarQuickAnswer* self ); + +G_DECLARE_FINAL_TYPE( FoobarQuickAnswerService, foobar_quick_answer_service, FOOBAR, QUICK_ANSWER_SERVICE, GObject ) + +FoobarQuickAnswerService* foobar_quick_answer_service_new ( void ); +FoobarQuickAnswer* foobar_quick_answer_service_query ( FoobarQuickAnswerService* self, + gchar const* query ); + +G_END_DECLS -- cgit v1.3.1 From 40939b3ebe359b19cc4ee1d9a7b831fada71ce75 Mon Sep 17 00:00:00 2001 From: Hannes Schulze Date: Wed, 3 Jul 2024 15:05:18 +0200 Subject: implement a parser for mathematical expressions --- flake.nix | 1 + meson.build | 1 + nix/package.nix | 3 +- src/meson.build | 1 + src/services/meson.build | 2 + src/services/quick-answer-service.c | 24 ++- src/services/quick-answers/math-lexer.c | 175 ++++++++++++++++ src/services/quick-answers/math-parser.c | 336 +++++++++++++++++++++++++++++++ src/services/quick-answers/math.c | 298 +++++++++++++++++++++++++++ src/services/quick-answers/math.h | 124 ++++++++++++ src/services/quick-answers/meson.build | 5 + 11 files changed, 961 insertions(+), 9 deletions(-) create mode 100644 src/services/quick-answers/math-lexer.c create mode 100644 src/services/quick-answers/math-parser.c create mode 100644 src/services/quick-answers/math.c create mode 100644 src/services/quick-answers/math.h create mode 100644 src/services/quick-answers/meson.build diff --git a/flake.nix b/flake.nix index 2468202..b63d871 100644 --- a/flake.nix +++ b/flake.nix @@ -28,6 +28,7 @@ packages = [ pkgs.libnotify # for testing the notification daemon pkgs.clang-tools + pkgs.gdb ]; inputsFrom = [ foobar ]; hardeningDisable = [ "fortify" ]; diff --git a/meson.build b/meson.build index d92faa4..457286b 100644 --- a/meson.build +++ b/meson.build @@ -8,6 +8,7 @@ gio_unix_dep = dependency('gio-unix-2.0', version: '>=2.78') gtk_dep = dependency('gtk4', version: '>=4.12') json_dep = dependency('json-glib-1.0', version: '>=1.8') nm_dep = dependency('libnm', version: '>=1.44') +gmp_dep = dependency('gmp', version: '>=6.3') m_dep = meson.get_compiler('c').find_library('m', required : false) layershell_dep = dependency('gtk4-layer-shell', required: false) gvc_dep = dependency('gvc', required: false) diff --git a/nix/package.nix b/nix/package.nix index 4c47e83..3b8be8f 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -15,6 +15,7 @@ glib, gtk4, json-glib, + gmp, librsvg, networkmanager, wayland, @@ -54,7 +55,7 @@ ''; nativeBuildInputs = [ makeWrapper git meson ninja vala sassc pkg-config gobject-introspection wayland-scanner ]; - buildInputs = [ glib gtk4 json-glib librsvg networkmanager wayland libpulseaudio alsa-lib upower brightnessctl ]; + buildInputs = [ glib gtk4 json-glib gmp librsvg networkmanager wayland libpulseaudio alsa-lib upower brightnessctl ]; meta = with lib; { homepage = "https://github.com/hannesschulze/foobar"; diff --git a/src/meson.build b/src/meson.build index 78e0575..9c6ef09 100644 --- a/src/meson.build +++ b/src/meson.build @@ -25,6 +25,7 @@ executable( gio_unix_dep, gtk_dep, json_dep, + gmp_dep, nm_dep, m_dep, layershell_dep, diff --git a/src/services/meson.build b/src/services/meson.build index e4f151d..368c2f1 100644 --- a/src/services/meson.build +++ b/src/services/meson.build @@ -11,3 +11,5 @@ foobar_sources += files( 'quick-answer-service.c', 'configuration-service.c', ) + +subdir('quick-answers') diff --git a/src/services/quick-answer-service.c b/src/services/quick-answer-service.c index b564a16..70d54b6 100644 --- a/src/services/quick-answer-service.c +++ b/src/services/quick-answer-service.c @@ -1,4 +1,5 @@ #include "services/quick-answer-service.h" +#include "services/quick-answers/math.h" #include "launcher-item.h" #include @@ -302,16 +303,23 @@ FoobarQuickAnswer* foobar_quick_answer_service_query( { (void)self; - if ( !g_strcmp0(query, "2+2") ) + gsize token_count; + g_autofree FoobarMathToken* tokens = foobar_math_lex( query, strlen( query ), &token_count ); + if ( tokens ) { - g_autoptr(GIcon) icon = g_themed_icon_new("fluent-calculator-symbolic"); - - FoobarQuickAnswer* answer = foobar_quick_answer_new(); - foobar_quick_answer_set_value(answer, "4"); - foobar_quick_answer_set_title(answer, "= 4"); - foobar_quick_answer_set_icon(answer, icon); - return answer; + FoobarMathExpression* expr = foobar_math_parse( tokens, token_count ); + if ( expr ) + { + g_print( "---\n" ); + foobar_math_expression_print( expr, 0 ); + foobar_math_expression_free( expr ); + } } + (void)foobar_quick_answer_new; + (void)foobar_quick_answer_set_value; + (void)foobar_quick_answer_set_icon; + (void)foobar_quick_answer_set_title; + return NULL; } diff --git a/src/services/quick-answers/math-lexer.c b/src/services/quick-answers/math-lexer.c new file mode 100644 index 0000000..b0d24b7 --- /dev/null +++ b/src/services/quick-answers/math-lexer.c @@ -0,0 +1,175 @@ +#include "services/quick-answers/math.h" + +typedef struct _Lexer Lexer; + +struct _Lexer +{ + GArray* result; + gchar const* input; + gsize input_length; + gsize position; + gsize token_start; +}; + +static gboolean char_is_whitespace( char c ); +static gboolean char_is_digit ( char c ); +static gboolean char_is_identifier( char c ); + +static char lexer_peek ( Lexer const* ctx, + gsize offset ); +static char lexer_pop ( Lexer* ctx ); +static void lexer_begin_token ( Lexer* ctx ); +static void lexer_commit_token ( Lexer* ctx, + FoobarMathTokenType type ); +static void lexer_commit_single( Lexer* ctx, + FoobarMathTokenType type ); + +FoobarMathToken* foobar_math_lex( + gchar const* input, + gsize input_length, + gsize* out_count ) +{ + g_autoptr( GArray ) result = g_array_new( FALSE, TRUE, sizeof( FoobarMathToken ) ); + + Lexer ctx = { 0 }; + ctx.result = result; + ctx.input = input; + ctx.input_length = input_length; + + while ( ctx.position < input_length ) + { + if ( char_is_whitespace( lexer_peek( &ctx, 0 ) ) ) + { + // Skip whitespace. + + lexer_pop( &ctx ); + } + else if ( char_is_digit( lexer_peek( &ctx, 0 ) ) ) + { + // Process numbers as a whole, don't allow whitespace inbetween. + + lexer_begin_token( &ctx ); + while ( char_is_digit( lexer_peek( &ctx, 0 ) ) ) + { + lexer_pop( &ctx ); + } + + if ( lexer_peek( &ctx, 0 ) == '.' && char_is_digit( lexer_peek( &ctx, 1 ) ) ) + { + lexer_pop( &ctx ); + while ( char_is_digit( lexer_peek( &ctx, 0 ) ) ) + { + lexer_pop( &ctx ); + } + } + + lexer_commit_token( &ctx, FOOBAR_MATH_TOKEN_NUMBER ); + } + else if ( char_is_identifier( lexer_peek( &ctx, 0 ) ) ) + { + // Don't validate identifiers, leave that to the parser. + + lexer_begin_token( &ctx ); + while ( char_is_identifier( lexer_peek( &ctx, 0 ) ) ) + { + lexer_pop( &ctx ); + } + lexer_commit_token( &ctx, FOOBAR_MATH_TOKEN_IDENTIFIER ); + } + else if ( lexer_peek( &ctx, 0 ) == '(' ) + { + lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_PAREN_OPEN ); + } + else if ( lexer_peek( &ctx, 0 ) == ')' ) + { + lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_PAREN_CLOSE ); + } + else if ( lexer_peek( &ctx, 0 ) == '+' ) + { + lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_PLUS ); + } + else if ( lexer_peek( &ctx, 0 ) == '-' ) + { + lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_MINUS ); + } + else if ( lexer_peek( &ctx, 0 ) == '*' ) + { + lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_MUL ); + } + else if ( lexer_peek( &ctx, 0 ) == '/' ) + { + lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_DIV ); + } + else if ( lexer_peek( &ctx, 0 ) == '^' ) + { + lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_POW ); + } + else + { + // Unknown character. + + return NULL; + } + } + + return g_array_steal( result, out_count ); +} + +gboolean char_is_whitespace( char c ) +{ + return c == ' ' || c == '\t' || c == '\r'; +} + +gboolean char_is_digit( char c ) +{ + return c >= '0' && c <= '9'; +} + +gboolean char_is_identifier( char c ) +{ + // Identifiers are all lowercase ASCII. + + return c >= 'a' && c <= 'z'; +} + +char lexer_peek( + Lexer const* ctx, + gsize offset ) +{ + gsize position = ctx->position + offset; + if ( position >= ctx->input_length ) { return 0; } + + return ctx->input[position]; +} + +char lexer_pop( Lexer* ctx ) +{ + if ( ctx->position >= ctx->input_length ) { return 0; } + + return ctx->input[ctx->position++]; +} + +void lexer_begin_token( Lexer* ctx ) +{ + ctx->token_start = ctx->position; +} + +void lexer_commit_token( + Lexer* ctx, + FoobarMathTokenType type ) +{ + FoobarMathToken token = { 0 }; + token.data = &ctx->input[ctx->token_start]; + token.length = ctx->position - ctx->token_start; + token.type = type; + g_array_append_val( ctx->result, token ); +} + +void lexer_commit_single( + Lexer* ctx, + FoobarMathTokenType type ) +{ + lexer_begin_token( ctx ); + lexer_pop( ctx ); + lexer_commit_token( ctx, type ); +} diff --git a/src/services/quick-answers/math-parser.c b/src/services/quick-answers/math-parser.c new file mode 100644 index 0000000..bb94a66 --- /dev/null +++ b/src/services/quick-answers/math-parser.c @@ -0,0 +1,336 @@ +#include "services/quick-answers/math.h" + +typedef struct _Parser Parser; + +struct _Parser +{ + FoobarMathToken const* tokens; + gsize tokens_count; + gsize position; +}; + +typedef enum +{ + INFIX_PRECEDENCE_INVALID = -1, + INFIX_PRECEDENCE_NONE = 0, + INFIX_PRECEDENCE_ADDITIVE = 1, + INFIX_PRECEDENCE_MULTIPLICATIVE = 2, + INFIX_PRECEDENCE_EXPONENTS = 3, +} InfixPrecedence; + +static FoobarMathToken const* parser_peek ( Parser const* ctx ); +static FoobarMathToken const* parser_pop ( Parser* ctx ); +static FoobarMathExpression* parser_process ( Parser* ctx, + FoobarMathExpression* lhs, + InfixPrecedence min_precedence ); +static FoobarMathExpression* parser_process_single ( Parser* ctx, + gboolean allow_sign ); +static FoobarMathExpression* parser_process_identifier ( Parser* ctx ); +static FoobarMathExpression* parser_process_number ( Parser* ctx ); +static FoobarMathExpression* parser_process_function ( Parser* ctx, + FoobarMathFunction function ); +static FoobarMathExpression* parser_process_parens ( Parser* ctx ); +static FoobarMathExpression* parser_process_sign ( Parser* ctx ); +static gboolean parser_match_identifier ( FoobarMathToken const* token, + gchar const* identifier ); +static InfixPrecedence parser_operator_precedence( FoobarMathToken const* token ); +static FoobarMathOperation parser_operator ( FoobarMathToken const* token ); + +FoobarMathExpression* foobar_math_parse( + FoobarMathToken const* tokens, + gsize tokens_count ) +{ + Parser ctx = { 0 }; + ctx.tokens = tokens; + ctx.tokens_count = tokens_count; + + FoobarMathExpression* lhs = parser_process_single( &ctx, TRUE ); + if ( !lhs ) { return NULL; } + + FoobarMathExpression* expr = parser_process( &ctx, lhs, INFIX_PRECEDENCE_NONE ); + if ( !expr ) { return NULL; } + + if ( parser_peek( &ctx ) ) + { + foobar_math_expression_free( expr ); + return NULL; + } + + return expr; +} + +FoobarMathToken const* parser_peek( Parser const* ctx ) +{ + if ( ctx->position >= ctx->tokens_count ) { return NULL; } + + return &ctx->tokens[ctx->position]; +} + +FoobarMathToken const* parser_pop( Parser* ctx ) +{ + if ( ctx->position >= ctx->tokens_count ) { return NULL; } + + return &ctx->tokens[ctx->position++]; +} + +FoobarMathExpression* parser_process( + Parser* ctx, + FoobarMathExpression* lhs, + InfixPrecedence min_precedence ) +{ + FoobarMathToken const* op = parser_peek( ctx ); + InfixPrecedence op_precedence = parser_operator_precedence( op ); + while ( op_precedence >= min_precedence ) + { + parser_pop( ctx ); + + FoobarMathExpression* rhs = parser_process_single( ctx, FALSE ); + if ( !rhs ) + { + foobar_math_expression_free( lhs ); + return NULL; + } + + FoobarMathToken const* next_op = parser_peek( ctx ); + while ( parser_operator_precedence( next_op ) > op_precedence ) + { + rhs = parser_process( ctx, rhs, op_precedence + 1 ); + if ( !rhs ) + { + foobar_math_expression_free( lhs ); + return NULL; + } + + next_op = parser_peek( ctx ); + } + + lhs = foobar_math_expression_operation( parser_operator( op ), lhs, rhs ); + + op = parser_peek( ctx ); + op_precedence = parser_operator_precedence( op ); + } + + return lhs; +} + +FoobarMathExpression* parser_process_single( + Parser* ctx, + gboolean allow_sign ) +{ + FoobarMathToken const* token = parser_peek( ctx ); + if ( !token ) { return NULL; } + + switch ( token->type ) + { + case FOOBAR_MATH_TOKEN_IDENTIFIER: + return parser_process_identifier( ctx ); + case FOOBAR_MATH_TOKEN_NUMBER: + return parser_process_number( ctx ); + case FOOBAR_MATH_TOKEN_PAREN_OPEN: + return parser_process_parens( ctx ); + case FOOBAR_MATH_TOKEN_MINUS: + case FOOBAR_MATH_TOKEN_PLUS: + if ( !allow_sign ) { return NULL; } + return parser_process_sign( ctx ); + case FOOBAR_MATH_TOKEN_PAREN_CLOSE: + case FOOBAR_MATH_TOKEN_MUL: + case FOOBAR_MATH_TOKEN_DIV: + case FOOBAR_MATH_TOKEN_POW: + return NULL; + default: + g_warn_if_reached( ); + return NULL; + } +} + +FoobarMathExpression* parser_process_identifier( Parser* ctx ) +{ + FoobarMathToken const* token = parser_pop( ctx ); + if ( parser_match_identifier( token, "pi" ) ) + { + return foobar_math_expression_constant( FOOBAR_MATH_CONSTANT_PI ); + } + else if ( parser_match_identifier( token, "e" ) ) + { + return foobar_math_expression_constant( FOOBAR_MATH_CONSTANT_E ); + } + else if ( parser_match_identifier( token, "exp" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_EXP ); + } + else if ( parser_match_identifier( token, "sqrt" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_SQRT ); + } + else if ( parser_match_identifier( token, "sin" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_SIN ); + } + else if ( parser_match_identifier( token, "cos" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_COS ); + } + else if ( parser_match_identifier( token, "sec" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_SEC ); + } + else if ( parser_match_identifier( token, "csc" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_CSC ); + } + else if ( parser_match_identifier( token, "tan" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_TAN ); + } + else if ( parser_match_identifier( token, "cot" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_COT ); + } + else if ( parser_match_identifier( token, "arcsin" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCSIN ); + } + else if ( parser_match_identifier( token, "arccos" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCCOS ); + } + else if ( parser_match_identifier( token, "arcsec" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCSEC ); + } + else if ( parser_match_identifier( token, "arccsc" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCCSC ); + } + else if ( parser_match_identifier( token, "arctan" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCTAN ); + } + else if ( parser_match_identifier( token, "arccot" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCCOT ); + } + else + { + return NULL; + } +} + +FoobarMathExpression* parser_process_number( Parser* ctx ) +{ + FoobarMathToken const* token = parser_pop( ctx ); + FoobarMathValue value; + if ( !foobar_math_value_from_string( token->data, token->length, &value ) ) + { + return NULL; + } + + return foobar_math_expression_value( value ); +} + +FoobarMathExpression* parser_process_function( + Parser* ctx, + FoobarMathFunction function ) +{ + FoobarMathToken const* opening_paren = parser_peek( ctx ); + if ( !opening_paren || opening_paren->type != FOOBAR_MATH_TOKEN_PAREN_OPEN ) + { + return NULL; + } + + FoobarMathExpression* arg = parser_process_parens( ctx ); + if ( !arg ) { return NULL; } + + return foobar_math_expression_function( function, arg ); +} + +FoobarMathExpression* parser_process_parens( Parser* ctx ) +{ + parser_pop( ctx ); + FoobarMathExpression* lhs = parser_process_single( ctx, TRUE ); + if ( !lhs ) { return NULL; } + FoobarMathExpression* result = parser_process( ctx, lhs, INFIX_PRECEDENCE_NONE ); + if ( !result ) { return NULL; } + FoobarMathToken const* closing_paren = parser_pop( ctx ); + if ( !closing_paren || closing_paren->type != FOOBAR_MATH_TOKEN_PAREN_CLOSE ) + { + foobar_math_expression_free( result ); + return NULL; + } + return result; +} + +FoobarMathExpression* parser_process_sign( Parser* ctx ) +{ + FoobarMathToken const* token = parser_pop( ctx ); + FoobarMathExpression* result = parser_process_single( ctx, FALSE ); + if ( !result ) { return NULL; } + + if ( token->type == FOOBAR_MATH_TOKEN_MINUS ) + { + return foobar_math_expression_function( FOOBAR_MATH_FUNCTION_NEGATE, result ); + } + else + { + return result; + } +} + +gboolean parser_match_identifier( + FoobarMathToken const* token, + gchar const* identifier ) +{ + if ( strlen( identifier ) != token->length ) { return FALSE; } + + return !strncmp( token->data, identifier, token->length ); +} + +InfixPrecedence parser_operator_precedence( FoobarMathToken const* token ) +{ + if ( token ) + { + switch ( token->type ) + { + case FOOBAR_MATH_TOKEN_PLUS: + case FOOBAR_MATH_TOKEN_MINUS: + return INFIX_PRECEDENCE_ADDITIVE; + case FOOBAR_MATH_TOKEN_MUL: + case FOOBAR_MATH_TOKEN_DIV: + return INFIX_PRECEDENCE_MULTIPLICATIVE; + case FOOBAR_MATH_TOKEN_POW: + return INFIX_PRECEDENCE_EXPONENTS; + case FOOBAR_MATH_TOKEN_IDENTIFIER: + case FOOBAR_MATH_TOKEN_NUMBER: + case FOOBAR_MATH_TOKEN_PAREN_OPEN: + case FOOBAR_MATH_TOKEN_PAREN_CLOSE: + default: + break; + } + } + + return INFIX_PRECEDENCE_INVALID; +} + +FoobarMathOperation parser_operator( FoobarMathToken const* token ) +{ + switch ( token->type ) + { + case FOOBAR_MATH_TOKEN_PLUS: + return FOOBAR_MATH_OPERATION_ADD; + case FOOBAR_MATH_TOKEN_MINUS: + return FOOBAR_MATH_OPERATION_SUB; + case FOOBAR_MATH_TOKEN_MUL: + return FOOBAR_MATH_OPERATION_MUL; + case FOOBAR_MATH_TOKEN_DIV: + return FOOBAR_MATH_OPERATION_DIV; + case FOOBAR_MATH_TOKEN_POW: + return FOOBAR_MATH_OPERATION_POW; + case FOOBAR_MATH_TOKEN_IDENTIFIER: + case FOOBAR_MATH_TOKEN_NUMBER: + case FOOBAR_MATH_TOKEN_PAREN_OPEN: + case FOOBAR_MATH_TOKEN_PAREN_CLOSE: + default: + g_warn_if_reached( ); + return 0; + } +} diff --git a/src/services/quick-answers/math.c b/src/services/quick-answers/math.c new file mode 100644 index 0000000..5ec79c0 --- /dev/null +++ b/src/services/quick-answers/math.c @@ -0,0 +1,298 @@ +#include "services/quick-answers/math.h" + +struct _FoobarMathExpression +{ + FoobarMathExpressionType type; + + union + { + struct + { + FoobarMathValue v; + } value; + + struct + { + FoobarMathFunction f; + FoobarMathExpression* input; + } function; + + struct + { + FoobarMathConstant c; + } constant; + + struct + { + FoobarMathOperation o; + FoobarMathExpression* lhs; + FoobarMathExpression* rhs; + } operation; + }; +}; + +FoobarMathExpression* foobar_math_expression_value( FoobarMathValue value ) +{ + FoobarMathExpression* res = g_new0( FoobarMathExpression, 1 ); + res->type = FOOBAR_MATH_EXPRESSION_VALUE; + res->value.v = value; + return res; +} + +FoobarMathExpression* foobar_math_expression_function( + FoobarMathFunction function, + FoobarMathExpression* input ) +{ + FoobarMathExpression* res = g_new0( FoobarMathExpression, 1 ); + res->type = FOOBAR_MATH_EXPRESSION_FUNCTION; + res->function.f = function; + res->function.input = input; + return res; +} + +FoobarMathExpression* foobar_math_expression_constant( FoobarMathConstant constant ) +{ + FoobarMathExpression* res = g_new0( FoobarMathExpression, 1 ); + res->type = FOOBAR_MATH_EXPRESSION_CONSTANT; + res->constant.c = constant; + return res; +} + +FoobarMathExpression* foobar_math_expression_operation( + FoobarMathOperation operation, + FoobarMathExpression* lhs, + FoobarMathExpression* rhs ) +{ + FoobarMathExpression* res = g_new0( FoobarMathExpression, 1 ); + res->type = FOOBAR_MATH_EXPRESSION_OPERATION; + res->operation.o = operation; + res->operation.lhs = lhs; + res->operation.rhs = rhs; + return res; +} + +void foobar_math_expression_free( FoobarMathExpression* expression ) +{ + if ( expression ) + { + switch ( expression->type ) + { + case FOOBAR_MATH_EXPRESSION_VALUE: + foobar_math_value_free( expression->value.v ); + break; + case FOOBAR_MATH_EXPRESSION_FUNCTION: + foobar_math_expression_free( expression->function.input ); + break; + case FOOBAR_MATH_EXPRESSION_CONSTANT: + break; + case FOOBAR_MATH_EXPRESSION_OPERATION: + foobar_math_expression_free( expression->operation.lhs ); + foobar_math_expression_free( expression->operation.rhs ); + break; + default: + g_warn_if_reached( ); + break; + } + + g_free( expression ); + } +} + +void foobar_math_expression_print( + FoobarMathExpression const* expr, + gint indentation ) +{ + for (gint i = 0; i < indentation; ++i) { g_print("| "); } + switch ( expr->type ) + { + case FOOBAR_MATH_EXPRESSION_VALUE: + { + g_autofree gchar* val = foobar_math_value_format( expr->value.v ); + g_print( "%s\n", val ); + break; + } + case FOOBAR_MATH_EXPRESSION_FUNCTION: + switch ( expr->function.f ) + { + case FOOBAR_MATH_FUNCTION_NEGATE: + g_print( "-\n" ); + break; + case FOOBAR_MATH_FUNCTION_SIN: + g_print( "sin\n" ); + break; + case FOOBAR_MATH_FUNCTION_COS: + g_print( "cos\n" ); + break; + case FOOBAR_MATH_FUNCTION_SEC: + g_print( "sec\n" ); + break; + case FOOBAR_MATH_FUNCTION_CSC: + g_print( "csc\n" ); + break; + case FOOBAR_MATH_FUNCTION_TAN: + g_print( "tan\n" ); + break; + case FOOBAR_MATH_FUNCTION_COT: + g_print( "cot\n" ); + break; + case FOOBAR_MATH_FUNCTION_ARCSIN: + g_print( "arcsin\n" ); + break; + case FOOBAR_MATH_FUNCTION_ARCCOS: + g_print( "arccos\n" ); + break; + case FOOBAR_MATH_FUNCTION_ARCSEC: + g_print( "arcsec\n" ); + break; + case FOOBAR_MATH_FUNCTION_ARCCSC: + g_print( "arccsc\n" ); + break; + case FOOBAR_MATH_FUNCTION_ARCTAN: + g_print( "arctan\n" ); + break; + case FOOBAR_MATH_FUNCTION_ARCCOT: + g_print( "arccot\n" ); + break; + case FOOBAR_MATH_FUNCTION_EXP: + g_print( "exp\n" ); + break; + case FOOBAR_MATH_FUNCTION_SQRT: + g_print( "sqrt\n" ); + break; + default: + g_warn_if_reached( ); + break; + } + foobar_math_expression_print( expr->function.input, indentation + 1 ); + break; + case FOOBAR_MATH_EXPRESSION_CONSTANT: + switch ( expr->constant.c ) + { + case FOOBAR_MATH_CONSTANT_PI: + g_print( "pi\n" ); + break; + case FOOBAR_MATH_CONSTANT_E: + g_print( "e\n" ); + break; + default: + g_warn_if_reached( ); + break; + } + break; + case FOOBAR_MATH_EXPRESSION_OPERATION: + switch ( expr->operation.o ) + { + case FOOBAR_MATH_OPERATION_ADD: + g_print( "+\n" ); + break; + case FOOBAR_MATH_OPERATION_SUB: + g_print( "-\n" ); + break; + case FOOBAR_MATH_OPERATION_MUL: + g_print( "*\n" ); + break; + case FOOBAR_MATH_OPERATION_DIV: + g_print( "/\n" ); + break; + case FOOBAR_MATH_OPERATION_POW: + g_print( "^\n" ); + break; + default: + g_warn_if_reached( ); + break; + } + foobar_math_expression_print( expr->operation.lhs, indentation + 1 ); + foobar_math_expression_print( expr->operation.rhs, indentation + 1 ); + break; + default: + g_warn_if_reached( ); + break; + } +} + +gboolean foobar_math_value_from_string( + gchar const* input, + gsize input_length, + FoobarMathValue* out_value ) +{ + g_autofree gchar* without_sep = g_new0( gchar, input_length + 1 ); + gsize sep_pos = input_length - 1; + gsize j = 0; + for ( gsize i = 0; i < input_length; ++i ) + { + if ( input[i] == '.' ) + { + sep_pos = i; + } + else + { + without_sep[j++] = input[i]; + } + } + + out_value->type = FOOBAR_MATH_VALUE_INT; + out_value->int_value.decimal_places = input_length - 1 - sep_pos; + mpz_init( out_value->int_value.v ); + if ( mpz_init_set_str( out_value->int_value.v, without_sep, 10 ) == -1 ) + { + mpz_clear( out_value->int_value.v ); + return FALSE; + } + + return TRUE; +} + +void foobar_math_value_free( FoobarMathValue value ) +{ + if ( value.type == FOOBAR_MATH_VALUE_INT ) + { + mpz_clear( value.int_value.v ); + } +} + +gboolean foobar_math_evaluate( + FoobarMathExpression const* expression, + FoobarMathValue* out_value ) +{ + return FALSE; +} + +gchar* foobar_math_value_format( FoobarMathValue value ) +{ + switch ( value.type ) + { + case FOOBAR_MATH_VALUE_INT: + { + size_t len = mpz_sizeinbase( value.int_value.v, 10 ); + gchar* result = g_new0( gchar, len + 3 ); // length + null terminator + sign + separator + mpz_get_str( result, 10, value.int_value.v ); + len = strlen( result ); + gboolean trailing_zeros = TRUE; + gsize decimal_places = value.int_value.decimal_places; + gsize actual_decimal_places = decimal_places; + for ( gsize i = 0; i < decimal_places; ++i ) + { + if ( trailing_zeros && result[len - 1 - i] == '0' ) + { + result[len - 1 - i] = '\0'; + actual_decimal_places -= 1; + } + else + { + trailing_zeros = FALSE; + } + } + if ( actual_decimal_places > 0 ) + { + memmove( &result[len + 1 - decimal_places], &result[len - decimal_places],decimal_places + 1 ); + result[len - decimal_places] = '.'; + } + return result; + } + case FOOBAR_MATH_VALUE_FLOAT: + return g_strdup_printf( "%Lf", value.float_value.v ); + default: + g_warn_if_reached( ); + return NULL; + } +} diff --git a/src/services/quick-answers/math.h b/src/services/quick-answers/math.h new file mode 100644 index 0000000..77c587a --- /dev/null +++ b/src/services/quick-answers/math.h @@ -0,0 +1,124 @@ +#pragma once + +#include +#include + +G_BEGIN_DECLS + +typedef enum +{ + FOOBAR_MATH_VALUE_INT, + FOOBAR_MATH_VALUE_FLOAT, +} FoobarMathValueType; + +typedef struct _FoobarMathValue FoobarMathValue; + +struct _FoobarMathValue +{ + FoobarMathValueType type; + + union + { + struct + { + mpz_t v; + gsize decimal_places; + } int_value; + + struct + { + long double v; + } float_value; + }; +}; + +typedef enum +{ + FOOBAR_MATH_TOKEN_IDENTIFIER, + FOOBAR_MATH_TOKEN_NUMBER, + FOOBAR_MATH_TOKEN_PAREN_OPEN, + FOOBAR_MATH_TOKEN_PAREN_CLOSE, + FOOBAR_MATH_TOKEN_PLUS, + FOOBAR_MATH_TOKEN_MINUS, + FOOBAR_MATH_TOKEN_MUL, + FOOBAR_MATH_TOKEN_DIV, + FOOBAR_MATH_TOKEN_POW, +} FoobarMathTokenType; + +typedef struct _FoobarMathToken FoobarMathToken; + +struct _FoobarMathToken +{ + FoobarMathTokenType type; + gchar const* data; + gsize length; +}; + +typedef enum +{ + FOOBAR_MATH_EXPRESSION_VALUE, + FOOBAR_MATH_EXPRESSION_FUNCTION, + FOOBAR_MATH_EXPRESSION_CONSTANT, + FOOBAR_MATH_EXPRESSION_OPERATION, +} FoobarMathExpressionType; + +typedef enum +{ + FOOBAR_MATH_FUNCTION_NEGATE, + FOOBAR_MATH_FUNCTION_SIN, + FOOBAR_MATH_FUNCTION_COS, + FOOBAR_MATH_FUNCTION_SEC, + FOOBAR_MATH_FUNCTION_CSC, + FOOBAR_MATH_FUNCTION_TAN, + FOOBAR_MATH_FUNCTION_COT, + FOOBAR_MATH_FUNCTION_ARCSIN, + FOOBAR_MATH_FUNCTION_ARCCOS, + FOOBAR_MATH_FUNCTION_ARCSEC, + FOOBAR_MATH_FUNCTION_ARCCSC, + FOOBAR_MATH_FUNCTION_ARCTAN, + FOOBAR_MATH_FUNCTION_ARCCOT, + FOOBAR_MATH_FUNCTION_EXP, + FOOBAR_MATH_FUNCTION_SQRT, +} FoobarMathFunction; + +typedef enum +{ + FOOBAR_MATH_CONSTANT_PI, + FOOBAR_MATH_CONSTANT_E, +} FoobarMathConstant; + +typedef enum +{ + FOOBAR_MATH_OPERATION_ADD, + FOOBAR_MATH_OPERATION_SUB, + FOOBAR_MATH_OPERATION_MUL, + FOOBAR_MATH_OPERATION_DIV, + FOOBAR_MATH_OPERATION_POW, +} FoobarMathOperation; + +typedef struct _FoobarMathExpression FoobarMathExpression; + +FoobarMathToken* foobar_math_lex ( gchar const* input, + gsize input_length, + gsize* out_count ); +FoobarMathExpression* foobar_math_parse ( FoobarMathToken const* tokens, + gsize tokens_count ); +FoobarMathExpression* foobar_math_expression_value ( FoobarMathValue value ); +FoobarMathExpression* foobar_math_expression_function ( FoobarMathFunction function, + FoobarMathExpression* input ); +FoobarMathExpression* foobar_math_expression_constant ( FoobarMathConstant constant ); +FoobarMathExpression* foobar_math_expression_operation( FoobarMathOperation operation, + FoobarMathExpression* lhs, + FoobarMathExpression* rhs ); +void foobar_math_expression_print ( FoobarMathExpression const* expr, + gint indentation ); +void foobar_math_expression_free ( FoobarMathExpression* expression ); +gboolean foobar_math_value_from_string ( gchar const* input, + gsize input_length, + FoobarMathValue* out_value ); +void foobar_math_value_free ( FoobarMathValue value ); +gchar* foobar_math_value_format ( FoobarMathValue value ); +gboolean foobar_math_evaluate ( FoobarMathExpression const* expression, + FoobarMathValue* out_value ); + +G_END_DECLS diff --git a/src/services/quick-answers/meson.build b/src/services/quick-answers/meson.build new file mode 100644 index 0000000..a1cc573 --- /dev/null +++ b/src/services/quick-answers/meson.build @@ -0,0 +1,5 @@ +foobar_sources += files( + 'math-lexer.c', + 'math-parser.c', + 'math.c', +) -- cgit v1.3.1 From 6967d26dd1841965a6e979897e282dd66e45ef5f Mon Sep 17 00:00:00 2001 From: Hannes Schulze Date: Wed, 3 Jul 2024 19:07:45 +0200 Subject: implement expression evaluation --- src/services/quick-answer-service.c | 40 +++- src/services/quick-answers/math-parser.c | 14 +- src/services/quick-answers/math.c | 355 +++++++++++++++++++++++++++++-- src/services/quick-answers/math.h | 67 ++++-- 4 files changed, 424 insertions(+), 52 deletions(-) diff --git a/src/services/quick-answer-service.c b/src/services/quick-answer-service.c index 70d54b6..8071998 100644 --- a/src/services/quick-answer-service.c +++ b/src/services/quick-answer-service.c @@ -53,8 +53,9 @@ struct _FoobarQuickAnswerService GObject parent_instance; }; -static void foobar_quick_answer_service_class_init( FoobarQuickAnswerServiceClass* klass ); -static void foobar_quick_answer_service_init ( FoobarQuickAnswerService* self ); +static void foobar_quick_answer_service_class_init( FoobarQuickAnswerServiceClass* klass ); +static void foobar_quick_answer_service_init ( FoobarQuickAnswerService* self ); +static FoobarQuickAnswer* foobar_quick_answer_service_query_math( gchar const* query ); G_DEFINE_FINAL_TYPE( FoobarQuickAnswerService, foobar_quick_answer_service, G_TYPE_OBJECT ) @@ -303,6 +304,19 @@ FoobarQuickAnswer* foobar_quick_answer_service_query( { (void)self; + FoobarQuickAnswer* result = foobar_quick_answer_service_query_math( query ); + if ( result ) { return result; } + + return NULL; +} + +// +// Try to parse the query as a mathematical expression and evaluate it. +// +FoobarQuickAnswer* foobar_quick_answer_service_query_math( gchar const* query ) +{ + FoobarQuickAnswer* result = NULL; + gsize token_count; g_autofree FoobarMathToken* tokens = foobar_math_lex( query, strlen( query ), &token_count ); if ( tokens ) @@ -310,16 +324,24 @@ FoobarQuickAnswer* foobar_quick_answer_service_query( FoobarMathExpression* expr = foobar_math_parse( tokens, token_count ); if ( expr ) { - g_print( "---\n" ); foobar_math_expression_print( expr, 0 ); + + FoobarMathValue val = { 0 }; + if ( foobar_math_evaluate( expr, &val ) ) + { + g_autofree gchar* value = foobar_math_value_to_string( val ); + g_autofree gchar* title = g_strdup_printf( "= %s", value ); + g_autoptr( GIcon ) icon = g_themed_icon_new( "fluent-calculator-symbolic" ); + result = foobar_quick_answer_new( ); + foobar_quick_answer_set_value( result, value ); + foobar_quick_answer_set_title( result, title ); + foobar_quick_answer_set_icon( result, icon ); + foobar_math_value_free( val ); + } + foobar_math_expression_free( expr ); } } - (void)foobar_quick_answer_new; - (void)foobar_quick_answer_set_value; - (void)foobar_quick_answer_set_icon; - (void)foobar_quick_answer_set_title; - - return NULL; + return result; } diff --git a/src/services/quick-answers/math-parser.c b/src/services/quick-answers/math-parser.c index bb94a66..7f93552 100644 --- a/src/services/quick-answers/math-parser.c +++ b/src/services/quick-answers/math-parser.c @@ -104,7 +104,7 @@ FoobarMathExpression* parser_process( next_op = parser_peek( ctx ); } - lhs = foobar_math_expression_operation( parser_operator( op ), lhs, rhs ); + lhs = foobar_math_expression_new_operation( parser_operator( op ), lhs, rhs ); op = parser_peek( ctx ); op_precedence = parser_operator_precedence( op ); @@ -148,11 +148,11 @@ FoobarMathExpression* parser_process_identifier( Parser* ctx ) FoobarMathToken const* token = parser_pop( ctx ); if ( parser_match_identifier( token, "pi" ) ) { - return foobar_math_expression_constant( FOOBAR_MATH_CONSTANT_PI ); + return foobar_math_expression_new_constant( FOOBAR_MATH_CONSTANT_PI ); } else if ( parser_match_identifier( token, "e" ) ) { - return foobar_math_expression_constant( FOOBAR_MATH_CONSTANT_E ); + return foobar_math_expression_new_constant( FOOBAR_MATH_CONSTANT_E ); } else if ( parser_match_identifier( token, "exp" ) ) { @@ -219,13 +219,13 @@ FoobarMathExpression* parser_process_identifier( Parser* ctx ) FoobarMathExpression* parser_process_number( Parser* ctx ) { FoobarMathToken const* token = parser_pop( ctx ); - FoobarMathValue value; + FoobarMathValue value = { 0 }; if ( !foobar_math_value_from_string( token->data, token->length, &value ) ) { return NULL; } - return foobar_math_expression_value( value ); + return foobar_math_expression_new_value( value ); } FoobarMathExpression* parser_process_function( @@ -241,7 +241,7 @@ FoobarMathExpression* parser_process_function( FoobarMathExpression* arg = parser_process_parens( ctx ); if ( !arg ) { return NULL; } - return foobar_math_expression_function( function, arg ); + return foobar_math_expression_new_function( function, arg ); } FoobarMathExpression* parser_process_parens( Parser* ctx ) @@ -268,7 +268,7 @@ FoobarMathExpression* parser_process_sign( Parser* ctx ) if ( token->type == FOOBAR_MATH_TOKEN_MINUS ) { - return foobar_math_expression_function( FOOBAR_MATH_FUNCTION_NEGATE, result ); + return foobar_math_expression_new_function( FOOBAR_MATH_FUNCTION_NEGATE, result ); } else { diff --git a/src/services/quick-answers/math.c b/src/services/quick-answers/math.c index 5ec79c0..4596af2 100644 --- a/src/services/quick-answers/math.c +++ b/src/services/quick-answers/math.c @@ -1,4 +1,7 @@ +#define _GNU_SOURCE + #include "services/quick-answers/math.h" +#include struct _FoobarMathExpression { @@ -31,7 +34,7 @@ struct _FoobarMathExpression }; }; -FoobarMathExpression* foobar_math_expression_value( FoobarMathValue value ) +FoobarMathExpression* foobar_math_expression_new_value( FoobarMathValue value ) { FoobarMathExpression* res = g_new0( FoobarMathExpression, 1 ); res->type = FOOBAR_MATH_EXPRESSION_VALUE; @@ -39,7 +42,7 @@ FoobarMathExpression* foobar_math_expression_value( FoobarMathValue value ) return res; } -FoobarMathExpression* foobar_math_expression_function( +FoobarMathExpression* foobar_math_expression_new_function( FoobarMathFunction function, FoobarMathExpression* input ) { @@ -50,7 +53,7 @@ FoobarMathExpression* foobar_math_expression_function( return res; } -FoobarMathExpression* foobar_math_expression_constant( FoobarMathConstant constant ) +FoobarMathExpression* foobar_math_expression_new_constant( FoobarMathConstant constant ) { FoobarMathExpression* res = g_new0( FoobarMathExpression, 1 ); res->type = FOOBAR_MATH_EXPRESSION_CONSTANT; @@ -58,7 +61,7 @@ FoobarMathExpression* foobar_math_expression_constant( FoobarMathConstant consta return res; } -FoobarMathExpression* foobar_math_expression_operation( +FoobarMathExpression* foobar_math_expression_new_operation( FoobarMathOperation operation, FoobarMathExpression* lhs, FoobarMathExpression* rhs ) @@ -107,7 +110,7 @@ void foobar_math_expression_print( { case FOOBAR_MATH_EXPRESSION_VALUE: { - g_autofree gchar* val = foobar_math_value_format( expr->value.v ); + g_autofree gchar* val = foobar_math_value_to_string( expr->value.v ); g_print( "%s\n", val ); break; } @@ -210,6 +213,13 @@ void foobar_math_expression_print( } } +void foobar_math_value_new_int( FoobarMathValue* out_value ) +{ + out_value->type = FOOBAR_MATH_VALUE_INT; + mpz_init( out_value->int_value.v ); + out_value->int_value.decimal_places = 0; +} + gboolean foobar_math_value_from_string( gchar const* input, gsize input_length, @@ -230,9 +240,8 @@ gboolean foobar_math_value_from_string( } } - out_value->type = FOOBAR_MATH_VALUE_INT; + foobar_math_value_new_int( out_value ); out_value->int_value.decimal_places = input_length - 1 - sep_pos; - mpz_init( out_value->int_value.v ); if ( mpz_init_set_str( out_value->int_value.v, without_sep, 10 ) == -1 ) { mpz_clear( out_value->int_value.v ); @@ -242,22 +251,173 @@ gboolean foobar_math_value_from_string( return TRUE; } +void foobar_math_value_from_float( + long double value, + FoobarMathValue* out_value ) +{ + out_value->type = FOOBAR_MATH_VALUE_FLOAT; + out_value->float_value.v = value; +} + +void foobar_math_value_copy( + FoobarMathValue value, + FoobarMathValue* out_value ) +{ + *out_value = value; + + switch ( value.type ) + { + case FOOBAR_MATH_VALUE_INT: + mpz_init_set( out_value->int_value.v, value.int_value.v ); + break; + case FOOBAR_MATH_VALUE_FLOAT: + break; + default: + g_warn_if_reached( ); + break; + } +} + void foobar_math_value_free( FoobarMathValue value ) { - if ( value.type == FOOBAR_MATH_VALUE_INT ) + switch ( value.type ) { - mpz_clear( value.int_value.v ); + case FOOBAR_MATH_VALUE_INT: + mpz_clear( value.int_value.v ); + break; + case FOOBAR_MATH_VALUE_FLOAT: + break; + default: + g_warn_if_reached( ); + break; } } -gboolean foobar_math_evaluate( - FoobarMathExpression const* expression, - FoobarMathValue* out_value ) +void foobar_math_value_negate( FoobarMathValue* value ) +{ + switch ( value->type ) + { + case FOOBAR_MATH_VALUE_INT: + mpz_neg( value->int_value.v, value->int_value.v ); + break; + case FOOBAR_MATH_VALUE_FLOAT: + value->float_value.v = -value->float_value.v; + break; + default: + g_warn_if_reached( ); + break; + } +} + +gboolean foobar_math_value_add( + FoobarMathValue lhs, + FoobarMathValue rhs, + FoobarMathValue* out_value ) +{ + if ( foobar_math_value_is_simple_int( lhs ) && foobar_math_value_is_simple_int( rhs ) ) + { + foobar_math_value_new_int( out_value ); + mpz_add( out_value->int_value.v, lhs.int_value.v, rhs.int_value.v ); + return TRUE; + } + + long double lhs_float = foobar_math_value_to_float( lhs ); + long double rhs_float = foobar_math_value_to_float( rhs ); + foobar_math_value_from_float( lhs_float + rhs_float, out_value ); + return TRUE; +} + +gboolean foobar_math_value_sub( + FoobarMathValue lhs, + FoobarMathValue rhs, + FoobarMathValue* out_value ) +{ + if ( foobar_math_value_is_simple_int( lhs ) && foobar_math_value_is_simple_int( rhs ) ) + { + foobar_math_value_new_int( out_value ); + mpz_sub( out_value->int_value.v, lhs.int_value.v, rhs.int_value.v ); + return TRUE; + } + + long double lhs_float = foobar_math_value_to_float( lhs ); + long double rhs_float = foobar_math_value_to_float( rhs ); + foobar_math_value_from_float( lhs_float - rhs_float, out_value ); + return TRUE; +} + +gboolean foobar_math_value_mul( + FoobarMathValue lhs, + FoobarMathValue rhs, + FoobarMathValue* out_value ) +{ + if ( foobar_math_value_is_simple_int( lhs ) && foobar_math_value_is_simple_int( rhs ) ) + { + foobar_math_value_new_int( out_value ); + mpz_mul( out_value->int_value.v, lhs.int_value.v, rhs.int_value.v ); + return TRUE; + } + + long double lhs_float = foobar_math_value_to_float( lhs ); + long double rhs_float = foobar_math_value_to_float( rhs ); + foobar_math_value_from_float( lhs_float * rhs_float, out_value ); + return TRUE; +} + +gboolean foobar_math_value_div( + FoobarMathValue lhs, + FoobarMathValue rhs, + FoobarMathValue* out_value ) +{ + if ( foobar_math_value_is_simple_int( lhs ) && foobar_math_value_is_simple_int( rhs ) ) + { + if ( mpz_sgn( rhs.int_value.v ) == 0 ) { return FALSE; } + + foobar_math_value_new_int( out_value ); + mpz_t rem; + mpz_init( rem ); + mpz_divmod( out_value->int_value.v, rem, lhs.int_value.v, rhs.int_value.v ); + gboolean result_is_int = mpz_sgn( rem ) == 0; + mpz_clear( rem ); + + if ( result_is_int ) { return TRUE; } + foobar_math_value_free( *out_value ); + } + + long double lhs_float = foobar_math_value_to_float( lhs ); + long double rhs_float = foobar_math_value_to_float( rhs ); + long double result = lhs_float / rhs_float; + if ( isnanl( result ) || isinfl( result ) ) { return FALSE; } + foobar_math_value_from_float( result, out_value ); + return TRUE; +} + +gboolean foobar_math_value_pow( + FoobarMathValue lhs, + FoobarMathValue rhs, + FoobarMathValue* out_value ) { - return FALSE; + if ( foobar_math_value_is_simple_int( lhs ) && foobar_math_value_is_simple_int( rhs ) && + mpz_fits_ulong_p( rhs.int_value.v ) ) + { + foobar_math_value_new_int( out_value ); + mpz_pow_ui( out_value->int_value.v, lhs.int_value.v, mpz_get_ui( rhs.int_value.v ) ); + return TRUE; + } + + long double lhs_float = foobar_math_value_to_float( lhs ); + long double rhs_float = foobar_math_value_to_float( rhs ); + long double result = powl( lhs_float, rhs_float ); + if ( isnanl( result ) || isinfl( result ) ) { return FALSE; } + foobar_math_value_from_float( result, out_value ); + return TRUE; +} + +gboolean foobar_math_value_is_simple_int( FoobarMathValue value ) +{ + return value.type == FOOBAR_MATH_VALUE_INT && value.int_value.decimal_places == 0; } -gchar* foobar_math_value_format( FoobarMathValue value ) +gchar* foobar_math_value_to_string( FoobarMathValue value ) { switch ( value.type ) { @@ -296,3 +456,170 @@ gchar* foobar_math_value_format( FoobarMathValue value ) return NULL; } } + +long double foobar_math_value_to_float( FoobarMathValue value ) +{ + switch ( value.type ) + { + case FOOBAR_MATH_VALUE_INT: + { + mpz_t exp, integer, frac; + mpz_inits( exp, integer, frac, NULL ); + mpz_ui_pow_ui( exp, 10, value.int_value.decimal_places ); + mpz_mod( frac, value.int_value.v, exp ); + mpz_div( integer, value.int_value.v, exp ); + mpf_t f_exp, f_frac; + mpf_inits( f_exp, f_frac, NULL ); + mpf_set_z( f_exp, exp ); + mpf_set_z( f_frac, frac ); + mpf_div( f_frac, f_frac, f_exp ); + long double result = mpz_get_d( integer ) + mpf_get_d( f_frac ); + mpf_clears( f_exp, f_frac, NULL ); + mpz_clears( exp, integer, frac, NULL ); + return result; + } + case FOOBAR_MATH_VALUE_FLOAT: + return value.float_value.v; + default: + g_warn_if_reached( ); + return 0; + } +} + +gboolean foobar_math_evaluate( + FoobarMathExpression const* expr, + FoobarMathValue* out_value ) +{ + switch ( expr->type ) + { + case FOOBAR_MATH_EXPRESSION_VALUE: + foobar_math_value_copy( expr->value.v, out_value ); + return TRUE; + case FOOBAR_MATH_EXPRESSION_CONSTANT: + switch ( expr->constant.c ) + { + case FOOBAR_MATH_CONSTANT_PI: + foobar_math_value_from_float( M_PIl, out_value ); + return TRUE; + case FOOBAR_MATH_CONSTANT_E: + foobar_math_value_from_float( M_El, out_value ); + return TRUE; + default: + g_warn_if_reached( ); + return FALSE; + } + case FOOBAR_MATH_EXPRESSION_FUNCTION: + { + FoobarMathValue input = { 0 }; + if ( !foobar_math_evaluate( expr->function.input, &input ) ) { return FALSE; } + + if ( expr->function.f == FOOBAR_MATH_FUNCTION_NEGATE ) + { + *out_value = input; + foobar_math_value_negate( out_value ); + return TRUE; + } + + long double input_float = foobar_math_value_to_float( input ); + foobar_math_value_free( input ); + + long double result = NAN; + switch ( expr->function.f ) + { + case FOOBAR_MATH_FUNCTION_SIN: + result = sinl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_COS: + result = cosl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_SEC: + result = 1.L / cosl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_CSC: + result = 1.L / sinl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_TAN: + result = tanl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_COT: + result = 1.L / tanl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_ARCSIN: + result = asinl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_ARCCOS: + result = acosl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_ARCSEC: + result = acosl( 1.L / input_float ); + break; + case FOOBAR_MATH_FUNCTION_ARCCSC: + result = asinl( 1.L / input_float ); + break; + case FOOBAR_MATH_FUNCTION_ARCTAN: + result = atanl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_ARCCOT: + result = atanl( 1.L / input_float ); + break; + case FOOBAR_MATH_FUNCTION_EXP: + result = expl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_SQRT: + result = sqrtl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_NEGATE: + default: + g_warn_if_reached( ); + return FALSE; + } + + if ( isnanl( result ) || isinfl( result ) ) { return FALSE; } + + foobar_math_value_from_float( result, out_value ); + return TRUE; + } + case FOOBAR_MATH_EXPRESSION_OPERATION: + { + FoobarMathValue lhs = { 0 }; + if ( !foobar_math_evaluate( expr->operation.lhs, &lhs ) ) { return FALSE; } + FoobarMathValue rhs = { 0 }; + if ( !foobar_math_evaluate( expr->operation.rhs, &rhs ) ) + { + foobar_math_value_free( lhs ); + return FALSE; + } + + gboolean success = FALSE; + switch ( expr->operation.o ) + { + case FOOBAR_MATH_OPERATION_ADD: + success = foobar_math_value_add( lhs, rhs, out_value ); + break; + case FOOBAR_MATH_OPERATION_SUB: + success = foobar_math_value_sub( lhs, rhs, out_value ); + break; + case FOOBAR_MATH_OPERATION_MUL: + success = foobar_math_value_mul( lhs, rhs, out_value ); + break; + case FOOBAR_MATH_OPERATION_DIV: + success = foobar_math_value_div( lhs, rhs, out_value ); + break; + case FOOBAR_MATH_OPERATION_POW: + success = foobar_math_value_pow( lhs, rhs, out_value ); + break; + default: + g_warn_if_reached( ); + break; + } + + foobar_math_value_free( lhs ); + foobar_math_value_free( rhs ); + + return success; + } + default: + g_warn_if_reached( ); + return FALSE; + } +} diff --git a/src/services/quick-answers/math.h b/src/services/quick-answers/math.h index 77c587a..6bc07fe 100644 --- a/src/services/quick-answers/math.h +++ b/src/services/quick-answers/math.h @@ -98,27 +98,50 @@ typedef enum typedef struct _FoobarMathExpression FoobarMathExpression; -FoobarMathToken* foobar_math_lex ( gchar const* input, - gsize input_length, - gsize* out_count ); -FoobarMathExpression* foobar_math_parse ( FoobarMathToken const* tokens, - gsize tokens_count ); -FoobarMathExpression* foobar_math_expression_value ( FoobarMathValue value ); -FoobarMathExpression* foobar_math_expression_function ( FoobarMathFunction function, - FoobarMathExpression* input ); -FoobarMathExpression* foobar_math_expression_constant ( FoobarMathConstant constant ); -FoobarMathExpression* foobar_math_expression_operation( FoobarMathOperation operation, - FoobarMathExpression* lhs, - FoobarMathExpression* rhs ); -void foobar_math_expression_print ( FoobarMathExpression const* expr, - gint indentation ); -void foobar_math_expression_free ( FoobarMathExpression* expression ); -gboolean foobar_math_value_from_string ( gchar const* input, - gsize input_length, - FoobarMathValue* out_value ); -void foobar_math_value_free ( FoobarMathValue value ); -gchar* foobar_math_value_format ( FoobarMathValue value ); -gboolean foobar_math_evaluate ( FoobarMathExpression const* expression, - FoobarMathValue* out_value ); +FoobarMathToken* foobar_math_lex ( gchar const* input, + gsize input_length, + gsize* out_count ); +FoobarMathExpression* foobar_math_parse ( FoobarMathToken const* tokens, + gsize tokens_count ); +FoobarMathExpression* foobar_math_expression_new_value ( FoobarMathValue value ); +FoobarMathExpression* foobar_math_expression_new_function ( FoobarMathFunction function, + FoobarMathExpression* input ); +FoobarMathExpression* foobar_math_expression_new_constant ( FoobarMathConstant constant ); +FoobarMathExpression* foobar_math_expression_new_operation( FoobarMathOperation operation, + FoobarMathExpression* lhs, + FoobarMathExpression* rhs ); +void foobar_math_expression_print ( FoobarMathExpression const* expr, + gint indentation ); +void foobar_math_expression_free ( FoobarMathExpression* expression ); +void foobar_math_value_new_int ( FoobarMathValue* out_value ); +gboolean foobar_math_value_from_string ( gchar const* input, + gsize input_length, + FoobarMathValue* out_value ); +void foobar_math_value_from_float ( long double value, + FoobarMathValue* out_value ); +void foobar_math_value_copy ( FoobarMathValue value, + FoobarMathValue* out_value ); +void foobar_math_value_free ( FoobarMathValue value ); +void foobar_math_value_negate ( FoobarMathValue* value ); +gboolean foobar_math_value_add ( FoobarMathValue lhs, + FoobarMathValue rhs, + FoobarMathValue* out_value ); +gboolean foobar_math_value_sub ( FoobarMathValue lhs, + FoobarMathValue rhs, + FoobarMathValue* out_value ); +gboolean foobar_math_value_mul ( FoobarMathValue lhs, + FoobarMathValue rhs, + FoobarMathValue* out_value ); +gboolean foobar_math_value_div ( FoobarMathValue lhs, + FoobarMathValue rhs, + FoobarMathValue* out_value ); +gboolean foobar_math_value_pow ( FoobarMathValue lhs, + FoobarMathValue rhs, + FoobarMathValue* out_value ); +gboolean foobar_math_value_is_simple_int ( FoobarMathValue value ); +gchar* foobar_math_value_to_string ( FoobarMathValue value ); +long double foobar_math_value_to_float ( FoobarMathValue value ); +gboolean foobar_math_evaluate ( FoobarMathExpression const* expr, + FoobarMathValue* out_value ); G_END_DECLS -- cgit v1.3.1 From 497f0388aee4b7ae0df92bb2b3617c524e5a5496 Mon Sep 17 00:00:00 2001 From: Hannes Schulze Date: Wed, 3 Jul 2024 22:44:40 +0200 Subject: increase floating point precision --- src/services/quick-answers/math.c | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/services/quick-answers/math.c b/src/services/quick-answers/math.c index 4596af2..42bc8bd 100644 --- a/src/services/quick-answers/math.c +++ b/src/services/quick-answers/math.c @@ -444,13 +444,36 @@ gchar* foobar_math_value_to_string( FoobarMathValue value ) } if ( actual_decimal_places > 0 ) { - memmove( &result[len + 1 - decimal_places], &result[len - decimal_places],decimal_places + 1 ); + memmove( &result[len + 1 - decimal_places], &result[len - decimal_places], decimal_places + 1 ); result[len - decimal_places] = '.'; } return result; } case FOOBAR_MATH_VALUE_FLOAT: - return g_strdup_printf( "%Lf", value.float_value.v ); + { + gchar* result = g_strdup_printf( "%.30Lf", value.float_value.v ); + + // trim trailing zeros + size_t len = strlen( result ); + for ( gsize i = 0; i < len; ++i ) + { + if ( result[len - 1 - i] == '.' ) + { + result[len - 1 - i] = '\0'; + break; + } + else if ( result[len - 1 - i] == '0' ) + { + result[len - 1 - i] = '\0'; + } + else + { + break; + } + } + + return result; + } default: g_warn_if_reached( ); return NULL; -- cgit v1.3.1 From e36217208802c001f1fbc758e5f103cbc4f339a2 Mon Sep 17 00:00:00 2001 From: Hannes Schulze Date: Sun, 3 Nov 2024 21:07:34 +0100 Subject: add unit tests for math --- flake.lock | 12 +-- meson.build | 26 +++--- src/meson.build | 77 ++++++++++++----- src/services/quick-answer-service.c | 15 +++- src/services/quick-answer-service.h | 2 +- src/services/quick-answers/math.c | 142 +++++++++++++++++++++--------- src/services/quick-answers/math.h | 23 ++--- src/services/quick-answers/math.test.c | 153 +++++++++++++++++++++++++++++++++ src/services/quick-answers/meson.build | 4 + subprojects/mutest.wrap | 4 + 10 files changed, 364 insertions(+), 94 deletions(-) create mode 100644 src/services/quick-answers/math.test.c create mode 100644 subprojects/mutest.wrap diff --git a/flake.lock b/flake.lock index 5005946..1ccee2c 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", "type": "github" }, "original": { @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1719506693, - "narHash": "sha256-C8e9S7RzshSdHB7L+v9I51af1gDM5unhJ2xO1ywxNH8=", + "lastModified": 1729880355, + "narHash": "sha256-RP+OQ6koQQLX5nw0NmcDrzvGL8HDLnyXt/jHhL1jwjM=", "owner": "nixos", "repo": "nixpkgs", - "rev": "b2852eb9365c6de48ffb0dc2c9562591f652242a", + "rev": "18536bf04cd71abd345f9579158841376fdd0c5a", "type": "github" }, "original": { diff --git a/meson.build b/meson.build index 457286b..f1cdb9d 100644 --- a/meson.build +++ b/meson.build @@ -10,16 +10,22 @@ json_dep = dependency('json-glib-1.0', version: '>=1.8') nm_dep = dependency('libnm', version: '>=1.44') gmp_dep = dependency('gmp', version: '>=6.3') m_dep = meson.get_compiler('c').find_library('m', required : false) -layershell_dep = dependency('gtk4-layer-shell', required: false) -gvc_dep = dependency('gvc', required: false) -if not layershell_dep.found() - layershell_proj = subproject('gtk4-layer-shell') - layershell_dep = layershell_proj.get_variable('gtk_layer_shell') -endif -if not gvc_dep.found() - gvc_proj = subproject('gvc') - gvc_dep = gvc_proj.get_variable('libgvc_dep') -endif +layershell_dep = dependency( + 'gtk4-layer-shell', + fallback: [ 'gtk4-layer-shell', 'gtk_layer_shell' ], + default_options: [ 'default_library=static' ], +) +gvc_dep = dependency( + 'gvc', + fallback: [ 'gvc', 'libgvc_dep' ], + default_options: [ 'default_library=static' ], +) +mutest_dep = dependency( + 'mutest', + fallback: [ 'mutest', 'mutest_dep' ], + default_options: [ 'static=true' ], + required: false, +) subdir('res') subdir('src') diff --git a/src/meson.build b/src/meson.build index 9c6ef09..b9383fb 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,5 +1,4 @@ foobar_sources = files( - 'main.c', 'application.c', 'utils.c', 'panel.c', @@ -8,36 +7,68 @@ foobar_sources = files( 'control-center.c', 'notification-area.c', ) +foobar_tests = {} subdir('dbus') subdir('services') subdir('widgets') +c_args = [ + '-Wall', + '-Werror', + '-Wswitch-enum', + '-Wswitch-default', + '-Wunused', + '-Wuninitialized', +] + +deps = [ + glib_dep, + gio_dep, + gio_unix_dep, + gtk_dep, + json_dep, + gmp_dep, + nm_dep, + m_dep, + layershell_dep, + gvc_dep, +] + +libfoobar = static_library( + meson.project_name(), + foobar_sources, + include_directories: include_directories('.'), + dependencies: deps, + install: false, + c_args: c_args +) + +libfoobar_dep = declare_dependency( + link_with: libfoobar, + include_directories: include_directories('.'), + dependencies: deps, +) + executable( meson.project_name(), + 'main.c', styles_resource, icons_resource, - foobar_sources, - include_directories: include_directories('.'), - dependencies: [ - glib_dep, - gio_dep, - gio_unix_dep, - gtk_dep, - json_dep, - gmp_dep, - nm_dep, - m_dep, - layershell_dep, - gvc_dep, - ], + dependencies: [ libfoobar_dep ], install: true, - c_args: [ - '-Wall', - '-Werror', - '-Wswitch-enum', - '-Wswitch-default', - '-Wunused', - '-Wuninitialized', - ], + c_args: c_args ) + +if mutest_dep.found() + foreach name, sources : foobar_tests + test( + name, + executable( + 'foobar-test-' + name, + sources, + dependencies: [ libfoobar_dep, mutest_dep ], + ), + ) + endforeach +endif diff --git a/src/services/quick-answer-service.c b/src/services/quick-answer-service.c index 8071998..670a72f 100644 --- a/src/services/quick-answer-service.c +++ b/src/services/quick-answer-service.c @@ -3,6 +3,12 @@ #include "launcher-item.h" #include +// +// FoobarQuickAnswer: +// +// A launcher item representing a quick, one-line answer to the query entered by the user. +// + struct _FoobarQuickAnswer { GObject parent_instance; @@ -48,6 +54,13 @@ G_DEFINE_FINAL_TYPE_WITH_CODE( G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE( FOOBAR_TYPE_LAUNCHER_ITEM, foobar_quick_answer_launcher_item_interface_init ) ) +// +// FoobarQuickAnswerService: +// +// Service providing quick answers for search queries in the launcher (if available). This includes evaluating +// mathematical expressions. +// + struct _FoobarQuickAnswerService { GObject parent_instance; @@ -324,7 +337,7 @@ FoobarQuickAnswer* foobar_quick_answer_service_query_math( gchar const* query ) FoobarMathExpression* expr = foobar_math_parse( tokens, token_count ); if ( expr ) { - foobar_math_expression_print( expr, 0 ); + // foobar_math_expression_print( expr, 0 ); FoobarMathValue val = { 0 }; if ( foobar_math_evaluate( expr, &val ) ) diff --git a/src/services/quick-answer-service.h b/src/services/quick-answer-service.h index 02a8e10..adb0701 100644 --- a/src/services/quick-answer-service.h +++ b/src/services/quick-answer-service.h @@ -16,6 +16,6 @@ G_DECLARE_FINAL_TYPE( FoobarQuickAnswerService, foobar_quick_answer_service, FOO FoobarQuickAnswerService* foobar_quick_answer_service_new ( void ); FoobarQuickAnswer* foobar_quick_answer_service_query ( FoobarQuickAnswerService* self, - gchar const* query ); + gchar const* query ); G_END_DECLS diff --git a/src/services/quick-answers/math.c b/src/services/quick-answers/math.c index 42bc8bd..e813808 100644 --- a/src/services/quick-answers/math.c +++ b/src/services/quick-answers/math.c @@ -310,72 +310,81 @@ void foobar_math_value_negate( FoobarMathValue* value ) } gboolean foobar_math_value_add( - FoobarMathValue lhs, - FoobarMathValue rhs, + FoobarMathValue* lhs, + FoobarMathValue* rhs, FoobarMathValue* out_value ) { - if ( foobar_math_value_is_simple_int( lhs ) && foobar_math_value_is_simple_int( rhs ) ) + if ( foobar_math_value_unify_integers( lhs, rhs ) ) { foobar_math_value_new_int( out_value ); - mpz_add( out_value->int_value.v, lhs.int_value.v, rhs.int_value.v ); + out_value->int_value.decimal_places = lhs->int_value.decimal_places; + mpz_add( out_value->int_value.v, lhs->int_value.v, rhs->int_value.v ); return TRUE; } - long double lhs_float = foobar_math_value_to_float( lhs ); - long double rhs_float = foobar_math_value_to_float( rhs ); + long double lhs_float = foobar_math_value_to_float( *lhs ); + long double rhs_float = foobar_math_value_to_float( *rhs ); foobar_math_value_from_float( lhs_float + rhs_float, out_value ); return TRUE; } gboolean foobar_math_value_sub( - FoobarMathValue lhs, - FoobarMathValue rhs, + FoobarMathValue* lhs, + FoobarMathValue* rhs, FoobarMathValue* out_value ) { - if ( foobar_math_value_is_simple_int( lhs ) && foobar_math_value_is_simple_int( rhs ) ) + if ( foobar_math_value_unify_integers( lhs, rhs ) ) { foobar_math_value_new_int( out_value ); - mpz_sub( out_value->int_value.v, lhs.int_value.v, rhs.int_value.v ); + out_value->int_value.decimal_places = lhs->int_value.decimal_places; + mpz_sub( out_value->int_value.v, lhs->int_value.v, rhs->int_value.v ); return TRUE; } - long double lhs_float = foobar_math_value_to_float( lhs ); - long double rhs_float = foobar_math_value_to_float( rhs ); + long double lhs_float = foobar_math_value_to_float( *lhs ); + long double rhs_float = foobar_math_value_to_float( *rhs ); foobar_math_value_from_float( lhs_float - rhs_float, out_value ); return TRUE; } gboolean foobar_math_value_mul( - FoobarMathValue lhs, - FoobarMathValue rhs, + FoobarMathValue* lhs, + FoobarMathValue* rhs, FoobarMathValue* out_value ) { - if ( foobar_math_value_is_simple_int( lhs ) && foobar_math_value_is_simple_int( rhs ) ) + if ( lhs->type == FOOBAR_MATH_VALUE_INT && rhs->type == FOOBAR_MATH_VALUE_INT ) { foobar_math_value_new_int( out_value ); - mpz_mul( out_value->int_value.v, lhs.int_value.v, rhs.int_value.v ); + out_value->int_value.decimal_places = lhs->int_value.decimal_places + rhs->int_value.decimal_places; + mpz_mul( out_value->int_value.v, lhs->int_value.v, rhs->int_value.v ); return TRUE; } - long double lhs_float = foobar_math_value_to_float( lhs ); - long double rhs_float = foobar_math_value_to_float( rhs ); + long double lhs_float = foobar_math_value_to_float( *lhs ); + long double rhs_float = foobar_math_value_to_float( *rhs ); foobar_math_value_from_float( lhs_float * rhs_float, out_value ); return TRUE; } gboolean foobar_math_value_div( - FoobarMathValue lhs, - FoobarMathValue rhs, + FoobarMathValue* lhs, + FoobarMathValue* rhs, FoobarMathValue* out_value ) { - if ( foobar_math_value_is_simple_int( lhs ) && foobar_math_value_is_simple_int( rhs ) ) + if ( lhs->type == FOOBAR_MATH_VALUE_INT && rhs->type == FOOBAR_MATH_VALUE_INT ) { - if ( mpz_sgn( rhs.int_value.v ) == 0 ) { return FALSE; } + if ( mpz_sgn( rhs->int_value.v ) == 0 ) { return FALSE; } + + if ( rhs->int_value.decimal_places > lhs->int_value.decimal_places ) + { + foobar_math_value_unify_integers( lhs, rhs ); + } foobar_math_value_new_int( out_value ); + out_value->int_value.decimal_places = lhs->int_value.decimal_places - rhs->int_value.decimal_places; mpz_t rem; mpz_init( rem ); - mpz_divmod( out_value->int_value.v, rem, lhs.int_value.v, rhs.int_value.v ); + mpz_divmod( out_value->int_value.v, rem, lhs->int_value.v, rhs->int_value.v ); gboolean result_is_int = mpz_sgn( rem ) == 0; mpz_clear( rem ); @@ -383,8 +392,8 @@ gboolean foobar_math_value_div( foobar_math_value_free( *out_value ); } - long double lhs_float = foobar_math_value_to_float( lhs ); - long double rhs_float = foobar_math_value_to_float( rhs ); + long double lhs_float = foobar_math_value_to_float( *lhs ); + long double rhs_float = foobar_math_value_to_float( *rhs ); long double result = lhs_float / rhs_float; if ( isnanl( result ) || isinfl( result ) ) { return FALSE; } foobar_math_value_from_float( result, out_value ); @@ -392,29 +401,60 @@ gboolean foobar_math_value_div( } gboolean foobar_math_value_pow( - FoobarMathValue lhs, - FoobarMathValue rhs, + FoobarMathValue* lhs, + FoobarMathValue* rhs, FoobarMathValue* out_value ) { - if ( foobar_math_value_is_simple_int( lhs ) && foobar_math_value_is_simple_int( rhs ) && - mpz_fits_ulong_p( rhs.int_value.v ) ) + if ( lhs->type == FOOBAR_MATH_VALUE_INT && + rhs->type == FOOBAR_MATH_VALUE_INT && + rhs->int_value.decimal_places == 0 && + mpz_fits_ulong_p( rhs->int_value.v ) ) { + unsigned long exp = mpz_get_ui( rhs->int_value.v ); foobar_math_value_new_int( out_value ); - mpz_pow_ui( out_value->int_value.v, lhs.int_value.v, mpz_get_ui( rhs.int_value.v ) ); + out_value->int_value.decimal_places = lhs->int_value.decimal_places * exp; + mpz_pow_ui( out_value->int_value.v, lhs->int_value.v, exp ); return TRUE; } - long double lhs_float = foobar_math_value_to_float( lhs ); - long double rhs_float = foobar_math_value_to_float( rhs ); + long double lhs_float = foobar_math_value_to_float( *lhs ); + long double rhs_float = foobar_math_value_to_float( *rhs ); long double result = powl( lhs_float, rhs_float ); if ( isnanl( result ) || isinfl( result ) ) { return FALSE; } foobar_math_value_from_float( result, out_value ); return TRUE; } -gboolean foobar_math_value_is_simple_int( FoobarMathValue value ) +gboolean foobar_math_value_unify_integers( + FoobarMathValue* a, + FoobarMathValue* b ) { - return value.type == FOOBAR_MATH_VALUE_INT && value.int_value.decimal_places == 0; + if ( a->type != FOOBAR_MATH_VALUE_INT || b->type != FOOBAR_MATH_VALUE_INT ) { return FALSE; } + + // Extend the less precise representation so both numbers have the same number of decimal places. + + gsize decimal_places = MAX( a->int_value.decimal_places, b->int_value.decimal_places ); + + mpz_t exp; + mpz_init( exp ); + + if ( decimal_places > a->int_value.decimal_places ) + { + mpz_ui_pow_ui( exp, 10, decimal_places - a->int_value.decimal_places ); + mpz_mul( a->int_value.v, a->int_value.v, exp ); + } + + if ( decimal_places > b->int_value.decimal_places ) + { + mpz_ui_pow_ui( exp, 10, decimal_places - b->int_value.decimal_places ); + mpz_mul( b->int_value.v, b->int_value.v, exp ); + } + + mpz_clear( exp ); + + a->int_value.decimal_places = b->int_value.decimal_places = decimal_places; + + return TRUE; } gchar* foobar_math_value_to_string( FoobarMathValue value ) @@ -423,10 +463,23 @@ gchar* foobar_math_value_to_string( FoobarMathValue value ) { case FOOBAR_MATH_VALUE_INT: { - size_t len = mpz_sizeinbase( value.int_value.v, 10 ); - gchar* result = g_new0( gchar, len + 3 ); // length + null terminator + sign + separator + gsize cap = mpz_sizeinbase( value.int_value.v, 10 ); + cap = MAX( cap, value.int_value.decimal_places + 1 ); // pad with zeroes if necessary + gchar* result = g_new0( gchar, cap + 3 ); // length + null terminator + sign + separator mpz_get_str( result, 10, value.int_value.v ); - len = strlen( result ); + gsize len = strlen( result ); // actual size may be 1 char less (+ len now also includes sign) + gboolean has_sign = result[0] == '-'; + if ( has_sign ) + { + // add the sign back later, makes everything else easier + result += 1; + len -= 1; + } + // pad with leading zeroes if necessary + gsize padded_len = MAX( len, value.int_value.decimal_places + 1 ); + memmove( &result[padded_len - len], result, len + 1 ); + memset( result, '0', padded_len - len ); + len = padded_len; gboolean trailing_zeros = TRUE; gsize decimal_places = value.int_value.decimal_places; gsize actual_decimal_places = decimal_places; @@ -447,6 +500,11 @@ gchar* foobar_math_value_to_string( FoobarMathValue value ) memmove( &result[len + 1 - decimal_places], &result[len - decimal_places], decimal_places + 1 ); result[len - decimal_places] = '.'; } + if ( has_sign ) + { + result -= 1; + len += 1; + } return result; } case FOOBAR_MATH_VALUE_FLOAT: @@ -617,19 +675,19 @@ gboolean foobar_math_evaluate( switch ( expr->operation.o ) { case FOOBAR_MATH_OPERATION_ADD: - success = foobar_math_value_add( lhs, rhs, out_value ); + success = foobar_math_value_add( &lhs, &rhs, out_value ); break; case FOOBAR_MATH_OPERATION_SUB: - success = foobar_math_value_sub( lhs, rhs, out_value ); + success = foobar_math_value_sub( &lhs, &rhs, out_value ); break; case FOOBAR_MATH_OPERATION_MUL: - success = foobar_math_value_mul( lhs, rhs, out_value ); + success = foobar_math_value_mul( &lhs, &rhs, out_value ); break; case FOOBAR_MATH_OPERATION_DIV: - success = foobar_math_value_div( lhs, rhs, out_value ); + success = foobar_math_value_div( &lhs, &rhs, out_value ); break; case FOOBAR_MATH_OPERATION_POW: - success = foobar_math_value_pow( lhs, rhs, out_value ); + success = foobar_math_value_pow( &lhs, &rhs, out_value ); break; default: g_warn_if_reached( ); diff --git a/src/services/quick-answers/math.h b/src/services/quick-answers/math.h index 6bc07fe..c66a2bf 100644 --- a/src/services/quick-answers/math.h +++ b/src/services/quick-answers/math.h @@ -123,22 +123,23 @@ void foobar_math_value_copy ( FoobarMathValue FoobarMathValue* out_value ); void foobar_math_value_free ( FoobarMathValue value ); void foobar_math_value_negate ( FoobarMathValue* value ); -gboolean foobar_math_value_add ( FoobarMathValue lhs, - FoobarMathValue rhs, +gboolean foobar_math_value_add ( FoobarMathValue* lhs, + FoobarMathValue* rhs, FoobarMathValue* out_value ); -gboolean foobar_math_value_sub ( FoobarMathValue lhs, - FoobarMathValue rhs, +gboolean foobar_math_value_sub ( FoobarMathValue* lhs, + FoobarMathValue* rhs, FoobarMathValue* out_value ); -gboolean foobar_math_value_mul ( FoobarMathValue lhs, - FoobarMathValue rhs, +gboolean foobar_math_value_mul ( FoobarMathValue* lhs, + FoobarMathValue* rhs, FoobarMathValue* out_value ); -gboolean foobar_math_value_div ( FoobarMathValue lhs, - FoobarMathValue rhs, +gboolean foobar_math_value_div ( FoobarMathValue* lhs, + FoobarMathValue* rhs, FoobarMathValue* out_value ); -gboolean foobar_math_value_pow ( FoobarMathValue lhs, - FoobarMathValue rhs, +gboolean foobar_math_value_pow ( FoobarMathValue* lhs, + FoobarMathValue* rhs, FoobarMathValue* out_value ); -gboolean foobar_math_value_is_simple_int ( FoobarMathValue value ); +gboolean foobar_math_value_unify_integers ( FoobarMathValue* a, + FoobarMathValue* b ); gchar* foobar_math_value_to_string ( FoobarMathValue value ); long double foobar_math_value_to_float ( FoobarMathValue value ); gboolean foobar_math_evaluate ( FoobarMathExpression const* expr, diff --git a/src/services/quick-answers/math.test.c b/src/services/quick-answers/math.test.c new file mode 100644 index 0000000..416b576 --- /dev/null +++ b/src/services/quick-answers/math.test.c @@ -0,0 +1,153 @@ +#include "services/quick-answers/math.h" +#include + +#define SERIALIZATION_TEST( value, identifier ) \ + static void serialization_value_##identifier##_spec( void ) \ + { \ + FoobarMathValue val; \ + \ + gchar const input[] = #value; \ + mutest_expect( \ + "parse result", \ + mutest_bool_value( foobar_math_value_from_string( input, sizeof(input) - 1, &val) ), \ + mutest_to_be_true, \ + NULL ); \ + g_autofree gchar* output = foobar_math_value_to_string( val ); \ + mutest_expect( \ + "string representation", \ + mutest_string_value( output ), \ + mutest_to_be, \ + input, \ + NULL ); \ + \ + foobar_math_value_free( val ); \ + } + +SERIALIZATION_TEST( 0, 0 ) +SERIALIZATION_TEST( 1, 1 ) +SERIALIZATION_TEST( -1, neg_1 ) +SERIALIZATION_TEST( 1.2, 1_2 ) +SERIALIZATION_TEST( -1.2, neg_1_2 ) +SERIALIZATION_TEST( 1.30002, 1_30002 ) +SERIALIZATION_TEST( -1.30002, neg_1_30002 ) +SERIALIZATION_TEST( 0.2, 0_2 ) +SERIALIZATION_TEST( -0.2, neg_0_2 ) +SERIALIZATION_TEST( 0.00002, 0_00002 ) +SERIALIZATION_TEST( -0.00002, neg_0_00002 ) +SERIALIZATION_TEST( 123, 123 ) +SERIALIZATION_TEST( 123.456, 123_456 ) +SERIALIZATION_TEST( -123, neg_123 ) +SERIALIZATION_TEST( -123.456, neg_123_456 ) + +#undef SERIALIZATION_TEST + +static void serialization_suite( void ) +{ + mutest_it( "value 0", serialization_value_0_spec ); + mutest_it( "value 1", serialization_value_1_spec ); + mutest_it( "value -1", serialization_value_neg_1_spec ); + mutest_it( "value 1.2", serialization_value_1_2_spec ); + mutest_it( "value -1.2", serialization_value_neg_1_2_spec ); + mutest_it( "value 1.30002", serialization_value_1_30002_spec ); + mutest_it( "value -1.30002", serialization_value_neg_1_30002_spec ); + mutest_it( "value 0.2", serialization_value_0_2_spec ); + mutest_it( "value -0.2", serialization_value_neg_0_2_spec ); + mutest_it( "value 0.00002", serialization_value_0_00002_spec ); + mutest_it( "value -0.00002", serialization_value_neg_0_00002_spec ); + mutest_it( "value 123", serialization_value_123_spec ); + mutest_it( "value 123.456", serialization_value_123_456_spec ); + mutest_it( "value -123", serialization_value_neg_123_spec ); + mutest_it( "value -123.456", serialization_value_neg_123_456_spec ); +} +#define OPERATION_TEST( op, value_a, value_b, value_res, identifier ) \ + static void operation_##identifier##_spec( void ) \ + { \ + FoobarMathValue a, b, res; \ + \ + gchar const input_a[] = #value_a; \ + gchar const input_b[] = #value_b; \ + mutest_expect( \ + "input a parsed", \ + mutest_bool_value( foobar_math_value_from_string( input_a, sizeof(input_a) - 1, &a) ), \ + mutest_to_be_true, \ + NULL ); \ + mutest_expect( \ + "input b parsed", \ + mutest_bool_value( foobar_math_value_from_string( input_b, sizeof(input_b) - 1, &b) ), \ + mutest_to_be_true, \ + NULL ); \ + foobar_math_value_##op( &a, &b, &res ); \ + g_autofree gchar* output = foobar_math_value_to_string( res ); \ + mutest_expect( \ + "result", \ + mutest_string_value( output ), \ + mutest_to_be, \ + #value_res, \ + NULL ); \ + \ + foobar_math_value_free( a ); \ + foobar_math_value_free( b ); \ + foobar_math_value_free( res ); \ + } + +OPERATION_TEST( add, 123, 456, 579, 123_plus_456) +OPERATION_TEST( add, 123.456, 11, 134.456, 123_456_plus_11) +OPERATION_TEST( add, 11, 123.456, 134.456, 11_plus_123_456) +OPERATION_TEST( sub, 123, 456, -333, 123_minus_456) +OPERATION_TEST( sub, 123.456, 11, 112.456, 123_456_minus_11) +OPERATION_TEST( sub, 11, 123.456, -112.456, 11_minus_123_456) +OPERATION_TEST( mul, 123, 3, 369, 123_times_3 ) +OPERATION_TEST( mul, 12, 0.05, 0.6, 12_times_0_05 ) +OPERATION_TEST( mul, 0.05, 12, 0.6, 0_05_times_12 ) +OPERATION_TEST( div, 369, 3, 123, 369_div_3 ) +OPERATION_TEST( div, 369, 2, 184.5, 369_div_2 ) +OPERATION_TEST( div, 12, 0.05, 240, 12_div_0_05 ) +OPERATION_TEST( div, 0.08, 4, 0.02, 0_08_div_4 ) +OPERATION_TEST( pow, 2, 3, 8, 2_pow_3 ) +OPERATION_TEST( pow, 4, 0.5, 2, 4_pow_0_5 ) + +#undef OPERATION_TEST + +static void addition_suite( void ) +{ + mutest_it( "calculate 123 + 456", operation_123_plus_456_spec ); + mutest_it( "calculate 123.456 + 11", operation_123_456_plus_11_spec ); + mutest_it( "calculate 11 + 123.456", operation_11_plus_123_456_spec ); +} + +static void subtraction_suite( void ) +{ + mutest_it( "calculate 123 - 456", operation_123_minus_456_spec ); + mutest_it( "calculate 123.456 - 11", operation_123_456_minus_11_spec ); + mutest_it( "calculate 11 - 123.456", operation_11_minus_123_456_spec ); +} + +static void multiplication_suite( void ) +{ + mutest_it( "calculate 123 * 3", operation_123_times_3_spec ); + mutest_it( "calculate 12 * 0.05", operation_12_times_0_05_spec ); + mutest_it( "calculate 0.05 * 12", operation_0_05_times_12_spec ); +} + +static void division_suite( void ) +{ + mutest_it( "calculate 369 / 3", operation_369_div_3_spec ); + mutest_it( "calculate 369 / 2", operation_369_div_2_spec ); + mutest_it( "calculate 12 / 0.05", operation_12_div_0_05_spec ); + mutest_it( "calculate 0.08 / 4", operation_0_08_div_4_spec ); +} + +static void power_suite( void ) +{ + mutest_it( "calculate 2 ^ 3", operation_2_pow_3_spec ); + mutest_it( "calculate 4 ^ 0.5", operation_4_pow_0_5_spec ); +} + +MUTEST_MAIN( + mutest_describe( "Serialization", serialization_suite ); + mutest_describe( "Addition", addition_suite ); + mutest_describe( "Subtraction", subtraction_suite ); + mutest_describe( "Multiplication", multiplication_suite ); + mutest_describe( "Division", division_suite ); + mutest_describe( "Power", power_suite ); +) diff --git a/src/services/quick-answers/meson.build b/src/services/quick-answers/meson.build index a1cc573..ae468c2 100644 --- a/src/services/quick-answers/meson.build +++ b/src/services/quick-answers/meson.build @@ -3,3 +3,7 @@ foobar_sources += files( 'math-parser.c', 'math.c', ) + +foobar_tests += { + 'math': files('math.test.c'), +} diff --git a/subprojects/mutest.wrap b/subprojects/mutest.wrap new file mode 100644 index 0000000..6e99973 --- /dev/null +++ b/subprojects/mutest.wrap @@ -0,0 +1,4 @@ +[wrap-git] +url = https://github.com/ebassi/mutest.git +revision = 18a20071773f7c4b75e82a931ef9b916b273b3e5 +depth = 1 -- cgit v1.3.1 From 83d650b82e5209d2ca24cc1e6bf04e0644b3f615 Mon Sep 17 00:00:00 2001 From: Hannes Schulze Date: Mon, 4 Nov 2024 12:51:05 +0100 Subject: add docs --- src/services/quick-answers/math-lexer.c | 73 ++++++ src/services/quick-answers/math-parser.c | 110 ++++++-- src/services/quick-answers/math.c | 437 ++++++++++++++++++++----------- src/services/quick-answers/math.h | 8 +- 4 files changed, 462 insertions(+), 166 deletions(-) diff --git a/src/services/quick-answers/math-lexer.c b/src/services/quick-answers/math-lexer.c index b0d24b7..7fbe877 100644 --- a/src/services/quick-answers/math-lexer.c +++ b/src/services/quick-answers/math-lexer.c @@ -1,5 +1,24 @@ #include "services/quick-answers/math.h" +// +// FoobarMathToken: +// +// A token in a mathematical expression. The token contents are left as-is and need not be null-terminated. For example, +// "sin(pi) - 3" would be represented as: +// - IDENTIFIER: "sin" +// - PAREN_OPEN: "(" +// - IDENTIFIER: "pi" +// - PAREN_CLOSE: ")" +// - MINUS: "-" +// - NUMBER: "3" +// + +// +// Lexer: +// +// The lexer context/state which is passed around. +// + typedef struct _Lexer Lexer; struct _Lexer @@ -24,6 +43,18 @@ static void lexer_commit_token ( Lexer* ctx, static void lexer_commit_single( Lexer* ctx, FoobarMathTokenType type ); +// --------------------------------------------------------------------------------------------------------------------- +// Lexing +// --------------------------------------------------------------------------------------------------------------------- + +// +// Divide an input string into a list of tokens. +// +// On success, this will return a newly allocated array of tokens which should be freed using g_free. The number of +// elements will be written into out_count. +// +// On error, this will return NULL. +// FoobarMathToken* foobar_math_lex( gchar const* input, gsize input_length, @@ -115,16 +146,29 @@ FoobarMathToken* foobar_math_lex( return g_array_steal( result, out_count ); } +// --------------------------------------------------------------------------------------------------------------------- +// Character Classification +// --------------------------------------------------------------------------------------------------------------------- + +// +// Check if a character is a whitespace symbol (space, tab, carriage return). +// gboolean char_is_whitespace( char c ) { return c == ' ' || c == '\t' || c == '\r'; } +// +// Check if a character is a decimal digit. +// gboolean char_is_digit( char c ) { return c >= '0' && c <= '9'; } +// +// Check if a character belongs to an identifier (like "sin", "pi", etc.). +// gboolean char_is_identifier( char c ) { // Identifiers are all lowercase ASCII. @@ -132,6 +176,15 @@ gboolean char_is_identifier( char c ) return c >= 'a' && c <= 'z'; } +// --------------------------------------------------------------------------------------------------------------------- +// State Helpers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Peek at the current character without consuming it. +// +// If this is the end of the input string, return 0. +// char lexer_peek( Lexer const* ctx, gsize offset ) @@ -142,6 +195,11 @@ char lexer_peek( return ctx->input[position]; } +// +// Consume the current character, moving forward to the next one. +// +// If this is the end of the input string, return 0. +// char lexer_pop( Lexer* ctx ) { if ( ctx->position >= ctx->input_length ) { return 0; } @@ -149,11 +207,21 @@ char lexer_pop( Lexer* ctx ) return ctx->input[ctx->position++]; } +// +// Start recording a token by remembering the current position. +// +// The current character will be the first one in the token. +// void lexer_begin_token( Lexer* ctx ) { ctx->token_start = ctx->position; } +// +// Stop recording a token, saving the string between the call to lexer_begin_token as the specified type. +// +// The current character will not be a part of the token. +// void lexer_commit_token( Lexer* ctx, FoobarMathTokenType type ) @@ -165,6 +233,11 @@ void lexer_commit_token( g_array_append_val( ctx->result, token ); } +// +// Consume the current character and record it as a token. +// +// This is equivalent to calling lexer_begin_token, lexer_pop, and lexer_commit_token. +// void lexer_commit_single( Lexer* ctx, FoobarMathTokenType type ) diff --git a/src/services/quick-answers/math-parser.c b/src/services/quick-answers/math-parser.c index 7f93552..17d976d 100644 --- a/src/services/quick-answers/math-parser.c +++ b/src/services/quick-answers/math-parser.c @@ -1,5 +1,11 @@ #include "services/quick-answers/math.h" +// +// Parser: +// +// The parser context/state which is passed around. +// + typedef struct _Parser Parser; struct _Parser @@ -9,6 +15,12 @@ struct _Parser gsize position; }; +// +// InfixPrecedence: +// +// Value indicating how strongly infix operators should bind. The higher the value, the stronger they should bind. +// + typedef enum { INFIX_PRECEDENCE_INVALID = -1, @@ -18,8 +30,6 @@ typedef enum INFIX_PRECEDENCE_EXPONENTS = 3, } InfixPrecedence; -static FoobarMathToken const* parser_peek ( Parser const* ctx ); -static FoobarMathToken const* parser_pop ( Parser* ctx ); static FoobarMathExpression* parser_process ( Parser* ctx, FoobarMathExpression* lhs, InfixPrecedence min_precedence ); @@ -31,11 +41,24 @@ static FoobarMathExpression* parser_process_function ( Parser* FoobarMathFunction function ); static FoobarMathExpression* parser_process_parens ( Parser* ctx ); static FoobarMathExpression* parser_process_sign ( Parser* ctx ); +static FoobarMathToken const* parser_peek ( Parser const* ctx ); +static FoobarMathToken const* parser_pop ( Parser* ctx ); static gboolean parser_match_identifier ( FoobarMathToken const* token, gchar const* identifier ); static InfixPrecedence parser_operator_precedence( FoobarMathToken const* token ); static FoobarMathOperation parser_operator ( FoobarMathToken const* token ); +// --------------------------------------------------------------------------------------------------------------------- +// Parsing +// --------------------------------------------------------------------------------------------------------------------- + +// +// Transform a sequence of tokens into a mathematical expression. +// +// On success, this will return a newly allocated expression which should be freed using foobar_math_expression_free. +// +// On error, this will return NULL. +// FoobarMathExpression* foobar_math_parse( FoobarMathToken const* tokens, gsize tokens_count ) @@ -59,20 +82,9 @@ FoobarMathExpression* foobar_math_parse( return expr; } -FoobarMathToken const* parser_peek( Parser const* ctx ) -{ - if ( ctx->position >= ctx->tokens_count ) { return NULL; } - - return &ctx->tokens[ctx->position]; -} - -FoobarMathToken const* parser_pop( Parser* ctx ) -{ - if ( ctx->position >= ctx->tokens_count ) { return NULL; } - - return &ctx->tokens[ctx->position++]; -} - +// +// Process tokens until the end of the current precedence scope (indicated by min_precedence), starting at an operator. +// FoobarMathExpression* parser_process( Parser* ctx, FoobarMathExpression* lhs, @@ -113,6 +125,12 @@ FoobarMathExpression* parser_process( return lhs; } +// +// Process a single value which may be either a constant, function, number, expression enclosed in parenthesis or +// negation. +// +// If an unexpected token is encountered (i.e. an operator), this will return FALSE. +// FoobarMathExpression* parser_process_single( Parser* ctx, gboolean allow_sign ) @@ -143,6 +161,11 @@ FoobarMathExpression* parser_process_single( } } +// +// Parse an identifier. +// +// If the identifier is a function, this will expected parenthesis and an expression afterwards. +// FoobarMathExpression* parser_process_identifier( Parser* ctx ) { FoobarMathToken const* token = parser_pop( ctx ); @@ -216,6 +239,9 @@ FoobarMathExpression* parser_process_identifier( Parser* ctx ) } } +// +// Process a numeric value. +// FoobarMathExpression* parser_process_number( Parser* ctx ) { FoobarMathToken const* token = parser_pop( ctx ); @@ -228,6 +254,9 @@ FoobarMathExpression* parser_process_number( Parser* ctx ) return foobar_math_expression_new_value( value ); } +// +// Process a function's parameter which should be enclosed in parenthesis. +// FoobarMathExpression* parser_process_function( Parser* ctx, FoobarMathFunction function ) @@ -244,6 +273,9 @@ FoobarMathExpression* parser_process_function( return foobar_math_expression_new_function( function, arg ); } +// +// Process an expression enclosed in parenthesis. +// FoobarMathExpression* parser_process_parens( Parser* ctx ) { parser_pop( ctx ); @@ -260,6 +292,9 @@ FoobarMathExpression* parser_process_parens( Parser* ctx ) return result; } +// +// Process a sign (- or +) which may be used for negation of an expression. +// FoobarMathExpression* parser_process_sign( Parser* ctx ) { FoobarMathToken const* token = parser_pop( ctx ); @@ -276,6 +311,43 @@ FoobarMathExpression* parser_process_sign( Parser* ctx ) } } +// --------------------------------------------------------------------------------------------------------------------- +// State Helpers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Peek at the current token without consuming it. +// +// If this is the end of the token list, return NULL. +// +FoobarMathToken const* parser_peek( Parser const* ctx ) +{ + if ( ctx->position >= ctx->tokens_count ) { return NULL; } + + return &ctx->tokens[ctx->position]; +} + +// +// Consume the current token. +// +// If this is the end of the token list, return NULL. +// +FoobarMathToken const* parser_pop( Parser* ctx ) +{ + if ( ctx->position >= ctx->tokens_count ) { return NULL; } + + return &ctx->tokens[ctx->position++]; +} + +// --------------------------------------------------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Match a token's string against the given identifier string. +// +// This is necessary because the token's content is not a null-terminated string. +// gboolean parser_match_identifier( FoobarMathToken const* token, gchar const* identifier ) @@ -285,6 +357,9 @@ gboolean parser_match_identifier( return !strncmp( token->data, identifier, token->length ); } +// +// Get the precedence of an infix operator token or INVALID. +// InfixPrecedence parser_operator_precedence( FoobarMathToken const* token ) { if ( token ) @@ -311,6 +386,9 @@ InfixPrecedence parser_operator_precedence( FoobarMathToken const* token ) return INFIX_PRECEDENCE_INVALID; } +// +// Get the operation for an infix operator token. This requires the token to be valid. +// FoobarMathOperation parser_operator( FoobarMathToken const* token ) { switch ( token->type ) diff --git a/src/services/quick-answers/math.c b/src/services/quick-answers/math.c index e813808..69219e2 100644 --- a/src/services/quick-answers/math.c +++ b/src/services/quick-answers/math.c @@ -3,6 +3,16 @@ #include "services/quick-answers/math.h" #include +// +// FoobarMathExpression: +// +// A fully parsed mathematical expression which can be evaluated, for example "sin(pi) - 3" would be represented as: +// - Operation: SUB +// - Function: SIN +// - Constant: PI +// - Value: 3 +// + struct _FoobarMathExpression { FoobarMathExpressionType type; @@ -34,6 +44,29 @@ struct _FoobarMathExpression }; }; +// +// FoobarMathValue: +// +// Representation of a value. The goal of this memory structure is to store the value as an integer for as long as +// possible. Because of this, there is a "decimal_places" field, so the actual value of an integer is +// $v * 10^(-decimal_places)$. +// +// If absolutely necessary (for example, because we use a function like sin), the value is converted to a floating-point +// value. +// +// Note that we use GMP to store the integer value to allow for arbitrarily large values. Because of this, the structure +// should always be freed using foobar_math_value_free. +// + +// --------------------------------------------------------------------------------------------------------------------- +// Expressions +// --------------------------------------------------------------------------------------------------------------------- + +// +// Helper to allocate a new FoobarMathExpression representing a value. +// +// Ownership of "value" is transferred to the new expression. +// FoobarMathExpression* foobar_math_expression_new_value( FoobarMathValue value ) { FoobarMathExpression* res = g_new0( FoobarMathExpression, 1 ); @@ -42,6 +75,12 @@ FoobarMathExpression* foobar_math_expression_new_value( FoobarMathValue value ) return res; } +// +// Helper to allocate a new FoobarMathExpression representing a function to be evaluated with another expression as its +// parameter. +// +// Ownership of "input" is transferred to the new expression. +// FoobarMathExpression* foobar_math_expression_new_function( FoobarMathFunction function, FoobarMathExpression* input ) @@ -53,6 +92,9 @@ FoobarMathExpression* foobar_math_expression_new_function( return res; } +// +// Helper to allocate a new FoobarMathExpression representing a known constant. +// FoobarMathExpression* foobar_math_expression_new_constant( FoobarMathConstant constant ) { FoobarMathExpression* res = g_new0( FoobarMathExpression, 1 ); @@ -61,6 +103,12 @@ FoobarMathExpression* foobar_math_expression_new_constant( FoobarMathConstant co return res; } +// +// Helper to allocate a new FoobarMathExpression representing an operation to be evaluated with two expressions as its +// parameters. +// +// Ownership of "lhs" and "rhs" is transferred to the new expression. +// FoobarMathExpression* foobar_math_expression_new_operation( FoobarMathOperation operation, FoobarMathExpression* lhs, @@ -74,6 +122,9 @@ FoobarMathExpression* foobar_math_expression_new_operation( return res; } +// +// Release the memory associated with a mathematical expression, along with the expressions it owns. +// void foobar_math_expression_free( FoobarMathExpression* expression ) { if ( expression ) @@ -101,6 +152,12 @@ void foobar_math_expression_free( FoobarMathExpression* expression ) } } +// +// Print a tree representation of a mathematical expression. +// +// This is mainly useful for debugging. "indentation" describes the current indentation level for the recursive call +// (should be 0 initially). +// void foobar_math_expression_print( FoobarMathExpression const* expr, gint indentation ) @@ -213,6 +270,158 @@ void foobar_math_expression_print( } } +// +// Evaluate an expression, producing a value. +// +// On success (indicated by the return value TRUE), the value should be freed using foobar_math_value_free. +// +gboolean foobar_math_evaluate( + FoobarMathExpression const* expr, + FoobarMathValue* out_value ) +{ + switch ( expr->type ) + { + case FOOBAR_MATH_EXPRESSION_VALUE: + foobar_math_value_copy( expr->value.v, out_value ); + return TRUE; + case FOOBAR_MATH_EXPRESSION_CONSTANT: + switch ( expr->constant.c ) + { + case FOOBAR_MATH_CONSTANT_PI: + foobar_math_value_from_float( M_PIl, out_value ); + return TRUE; + case FOOBAR_MATH_CONSTANT_E: + foobar_math_value_from_float( M_El, out_value ); + return TRUE; + default: + g_warn_if_reached( ); + return FALSE; + } + case FOOBAR_MATH_EXPRESSION_FUNCTION: + { + FoobarMathValue input = { 0 }; + if ( !foobar_math_evaluate( expr->function.input, &input ) ) { return FALSE; } + + if ( expr->function.f == FOOBAR_MATH_FUNCTION_NEGATE ) + { + *out_value = input; + foobar_math_value_negate( out_value ); + return TRUE; + } + + long double input_float = foobar_math_value_to_float( input ); + foobar_math_value_free( input ); + + long double result = NAN; + switch ( expr->function.f ) + { + case FOOBAR_MATH_FUNCTION_SIN: + result = sinl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_COS: + result = cosl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_SEC: + result = 1.L / cosl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_CSC: + result = 1.L / sinl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_TAN: + result = tanl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_COT: + result = 1.L / tanl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_ARCSIN: + result = asinl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_ARCCOS: + result = acosl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_ARCSEC: + result = acosl( 1.L / input_float ); + break; + case FOOBAR_MATH_FUNCTION_ARCCSC: + result = asinl( 1.L / input_float ); + break; + case FOOBAR_MATH_FUNCTION_ARCTAN: + result = atanl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_ARCCOT: + result = atanl( 1.L / input_float ); + break; + case FOOBAR_MATH_FUNCTION_EXP: + result = expl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_SQRT: + result = sqrtl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_NEGATE: + default: + g_warn_if_reached( ); + return FALSE; + } + + if ( isnanl( result ) || isinfl( result ) ) { return FALSE; } + + foobar_math_value_from_float( result, out_value ); + return TRUE; + } + case FOOBAR_MATH_EXPRESSION_OPERATION: + { + FoobarMathValue lhs = { 0 }; + if ( !foobar_math_evaluate( expr->operation.lhs, &lhs ) ) { return FALSE; } + FoobarMathValue rhs = { 0 }; + if ( !foobar_math_evaluate( expr->operation.rhs, &rhs ) ) + { + foobar_math_value_free( lhs ); + return FALSE; + } + + gboolean success = FALSE; + switch ( expr->operation.o ) + { + case FOOBAR_MATH_OPERATION_ADD: + success = foobar_math_value_add( &lhs, &rhs, out_value ); + break; + case FOOBAR_MATH_OPERATION_SUB: + success = foobar_math_value_sub( &lhs, &rhs, out_value ); + break; + case FOOBAR_MATH_OPERATION_MUL: + success = foobar_math_value_mul( &lhs, &rhs, out_value ); + break; + case FOOBAR_MATH_OPERATION_DIV: + success = foobar_math_value_div( &lhs, &rhs, out_value ); + break; + case FOOBAR_MATH_OPERATION_POW: + success = foobar_math_value_pow( &lhs, &rhs, out_value ); + break; + default: + g_warn_if_reached( ); + break; + } + + foobar_math_value_free( lhs ); + foobar_math_value_free( rhs ); + + return success; + } + default: + g_warn_if_reached( ); + return FALSE; + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Values +// --------------------------------------------------------------------------------------------------------------------- + +// +// Initialize a new integer value to zero. +// +// After this, the structure should be freed using foobar_math_value_free. +// void foobar_math_value_new_int( FoobarMathValue* out_value ) { out_value->type = FOOBAR_MATH_VALUE_INT; @@ -220,6 +429,24 @@ void foobar_math_value_new_int( FoobarMathValue* out_value ) out_value->int_value.decimal_places = 0; } +// +// Initialize a new value from its floating point representation. +// +// Although technically not necessary, you should still call foobar_math_value_free on the returned value. +// +void foobar_math_value_from_float( + long double value, + FoobarMathValue* out_value ) +{ + out_value->type = FOOBAR_MATH_VALUE_FLOAT; + out_value->float_value.v = value; +} + +// +// Parse a value from its string representation. This will always return an integer representation. +// +// On success (return value TRUE), the structure should be freed using foobar_math_value_free. +// gboolean foobar_math_value_from_string( gchar const* input, gsize input_length, @@ -251,14 +478,11 @@ gboolean foobar_math_value_from_string( return TRUE; } -void foobar_math_value_from_float( - long double value, - FoobarMathValue* out_value ) -{ - out_value->type = FOOBAR_MATH_VALUE_FLOAT; - out_value->float_value.v = value; -} - +// +// Create a copy of a value, allocating necessary memory. +// +// After this, the structure should be freed using foobar_math_value_free. +// void foobar_math_value_copy( FoobarMathValue value, FoobarMathValue* out_value ) @@ -278,6 +502,11 @@ void foobar_math_value_copy( } } +// +// Release memory associated with a value. +// +// This is necessary because the integer representation is stored using GMP, which can store arbitrarily large values. +// void foobar_math_value_free( FoobarMathValue value ) { switch ( value.type ) @@ -293,6 +522,13 @@ void foobar_math_value_free( FoobarMathValue value ) } } +// --------------------------------------------------------------------------------------------------------------------- +// Operations +// --------------------------------------------------------------------------------------------------------------------- + +// +// Operation equivalent to "value *= -1". +// void foobar_math_value_negate( FoobarMathValue* value ) { switch ( value->type ) @@ -309,6 +545,11 @@ void foobar_math_value_negate( FoobarMathValue* value ) } } +// +// Operation setting "out_value = lhs + rhs". +// +// After this, the result value should be freed using foobar_math_value_free. +// gboolean foobar_math_value_add( FoobarMathValue* lhs, FoobarMathValue* rhs, @@ -328,6 +569,11 @@ gboolean foobar_math_value_add( return TRUE; } +// +// Operation setting "out_value = lhs - rhs". +// +// After this, the result value should be freed using foobar_math_value_free. +// gboolean foobar_math_value_sub( FoobarMathValue* lhs, FoobarMathValue* rhs, @@ -347,6 +593,11 @@ gboolean foobar_math_value_sub( return TRUE; } +// +// Operation setting "out_value = lhs * rhs". +// +// After this, the result value should be freed using foobar_math_value_free. +// gboolean foobar_math_value_mul( FoobarMathValue* lhs, FoobarMathValue* rhs, @@ -366,6 +617,11 @@ gboolean foobar_math_value_mul( return TRUE; } +// +// Operation setting "out_value = lhs / rhs". +// +// After this, the result value should be freed using foobar_math_value_free. +// gboolean foobar_math_value_div( FoobarMathValue* lhs, FoobarMathValue* rhs, @@ -400,6 +656,11 @@ gboolean foobar_math_value_div( return TRUE; } +// +// Operation setting "out_value = lhs ^ rhs". +// +// After this, the result value should be freed using foobar_math_value_free. +// gboolean foobar_math_value_pow( FoobarMathValue* lhs, FoobarMathValue* rhs, @@ -425,6 +686,14 @@ gboolean foobar_math_value_pow( return TRUE; } +// +// Ensure that two integer values have the same number of decimal places. +// +// This may change the in-memory-representation of a value, but not its actual value. It only increases the precision of +// one value +// +// If either a or b is not stored as an integer, this will return FALSE. +// gboolean foobar_math_value_unify_integers( FoobarMathValue* a, FoobarMathValue* b ) @@ -457,6 +726,15 @@ gboolean foobar_math_value_unify_integers( return TRUE; } +// --------------------------------------------------------------------------------------------------------------------- +// Conversions +// --------------------------------------------------------------------------------------------------------------------- + +// +// Format a value as a string. +// +// The result is a null-terminated string which should be freed using g_free. +// gchar* foobar_math_value_to_string( FoobarMathValue value ) { switch ( value.type ) @@ -538,6 +816,11 @@ gchar* foobar_math_value_to_string( FoobarMathValue value ) } } +// +// Convert a value to its floating-point representation. +// +// This may be a lossy conversion and should only be done if absolutely necessary. +// long double foobar_math_value_to_float( FoobarMathValue value ) { switch ( value.type ) @@ -566,141 +849,3 @@ long double foobar_math_value_to_float( FoobarMathValue value ) return 0; } } - -gboolean foobar_math_evaluate( - FoobarMathExpression const* expr, - FoobarMathValue* out_value ) -{ - switch ( expr->type ) - { - case FOOBAR_MATH_EXPRESSION_VALUE: - foobar_math_value_copy( expr->value.v, out_value ); - return TRUE; - case FOOBAR_MATH_EXPRESSION_CONSTANT: - switch ( expr->constant.c ) - { - case FOOBAR_MATH_CONSTANT_PI: - foobar_math_value_from_float( M_PIl, out_value ); - return TRUE; - case FOOBAR_MATH_CONSTANT_E: - foobar_math_value_from_float( M_El, out_value ); - return TRUE; - default: - g_warn_if_reached( ); - return FALSE; - } - case FOOBAR_MATH_EXPRESSION_FUNCTION: - { - FoobarMathValue input = { 0 }; - if ( !foobar_math_evaluate( expr->function.input, &input ) ) { return FALSE; } - - if ( expr->function.f == FOOBAR_MATH_FUNCTION_NEGATE ) - { - *out_value = input; - foobar_math_value_negate( out_value ); - return TRUE; - } - - long double input_float = foobar_math_value_to_float( input ); - foobar_math_value_free( input ); - - long double result = NAN; - switch ( expr->function.f ) - { - case FOOBAR_MATH_FUNCTION_SIN: - result = sinl( input_float ); - break; - case FOOBAR_MATH_FUNCTION_COS: - result = cosl( input_float ); - break; - case FOOBAR_MATH_FUNCTION_SEC: - result = 1.L / cosl( input_float ); - break; - case FOOBAR_MATH_FUNCTION_CSC: - result = 1.L / sinl( input_float ); - break; - case FOOBAR_MATH_FUNCTION_TAN: - result = tanl( input_float ); - break; - case FOOBAR_MATH_FUNCTION_COT: - result = 1.L / tanl( input_float ); - break; - case FOOBAR_MATH_FUNCTION_ARCSIN: - result = asinl( input_float ); - break; - case FOOBAR_MATH_FUNCTION_ARCCOS: - result = acosl( input_float ); - break; - case FOOBAR_MATH_FUNCTION_ARCSEC: - result = acosl( 1.L / input_float ); - break; - case FOOBAR_MATH_FUNCTION_ARCCSC: - result = asinl( 1.L / input_float ); - break; - case FOOBAR_MATH_FUNCTION_ARCTAN: - result = atanl( input_float ); - break; - case FOOBAR_MATH_FUNCTION_ARCCOT: - result = atanl( 1.L / input_float ); - break; - case FOOBAR_MATH_FUNCTION_EXP: - result = expl( input_float ); - break; - case FOOBAR_MATH_FUNCTION_SQRT: - result = sqrtl( input_float ); - break; - case FOOBAR_MATH_FUNCTION_NEGATE: - default: - g_warn_if_reached( ); - return FALSE; - } - - if ( isnanl( result ) || isinfl( result ) ) { return FALSE; } - - foobar_math_value_from_float( result, out_value ); - return TRUE; - } - case FOOBAR_MATH_EXPRESSION_OPERATION: - { - FoobarMathValue lhs = { 0 }; - if ( !foobar_math_evaluate( expr->operation.lhs, &lhs ) ) { return FALSE; } - FoobarMathValue rhs = { 0 }; - if ( !foobar_math_evaluate( expr->operation.rhs, &rhs ) ) - { - foobar_math_value_free( lhs ); - return FALSE; - } - - gboolean success = FALSE; - switch ( expr->operation.o ) - { - case FOOBAR_MATH_OPERATION_ADD: - success = foobar_math_value_add( &lhs, &rhs, out_value ); - break; - case FOOBAR_MATH_OPERATION_SUB: - success = foobar_math_value_sub( &lhs, &rhs, out_value ); - break; - case FOOBAR_MATH_OPERATION_MUL: - success = foobar_math_value_mul( &lhs, &rhs, out_value ); - break; - case FOOBAR_MATH_OPERATION_DIV: - success = foobar_math_value_div( &lhs, &rhs, out_value ); - break; - case FOOBAR_MATH_OPERATION_POW: - success = foobar_math_value_pow( &lhs, &rhs, out_value ); - break; - default: - g_warn_if_reached( ); - break; - } - - foobar_math_value_free( lhs ); - foobar_math_value_free( rhs ); - - return success; - } - default: - g_warn_if_reached( ); - return FALSE; - } -} diff --git a/src/services/quick-answers/math.h b/src/services/quick-answers/math.h index c66a2bf..fafa73d 100644 --- a/src/services/quick-answers/math.h +++ b/src/services/quick-answers/math.h @@ -112,13 +112,15 @@ FoobarMathExpression* foobar_math_expression_new_operation( FoobarMathOperation FoobarMathExpression* rhs ); void foobar_math_expression_print ( FoobarMathExpression const* expr, gint indentation ); +gboolean foobar_math_evaluate ( FoobarMathExpression const* expr, + FoobarMathValue* out_value ); void foobar_math_expression_free ( FoobarMathExpression* expression ); void foobar_math_value_new_int ( FoobarMathValue* out_value ); +void foobar_math_value_from_float ( long double value, + FoobarMathValue* out_value ); gboolean foobar_math_value_from_string ( gchar const* input, gsize input_length, FoobarMathValue* out_value ); -void foobar_math_value_from_float ( long double value, - FoobarMathValue* out_value ); void foobar_math_value_copy ( FoobarMathValue value, FoobarMathValue* out_value ); void foobar_math_value_free ( FoobarMathValue value ); @@ -142,7 +144,5 @@ gboolean foobar_math_value_unify_integers ( FoobarMathValue* FoobarMathValue* b ); gchar* foobar_math_value_to_string ( FoobarMathValue value ); long double foobar_math_value_to_float ( FoobarMathValue value ); -gboolean foobar_math_evaluate ( FoobarMathExpression const* expr, - FoobarMathValue* out_value ); G_END_DECLS -- cgit v1.3.1