diff --git a/crates/amalthea/src/comm/data_explorer_comm.rs b/crates/amalthea/src/comm/data_explorer_comm.rs index 56dd29927..d9d628184 100644 --- a/crates/amalthea/src/comm/data_explorer_comm.rs +++ b/crates/amalthea/src/comm/data_explorer_comm.rs @@ -92,9 +92,12 @@ pub struct BackendState { /// requests. This parameter may change. pub connected: Option, - /// Optional experimental parameter to provide an explanation when - /// connected=false. This parameter may change. - pub error_message: Option + /// Optional experimental parameter to provide an explanation when + /// connected=false. This parameter may change. + pub error_message: Option, + + /// Optional formatting options for frontend display + pub format_options: Option, } /// Schema for a column in a table diff --git a/crates/ark/src/data_explorer/r_data_explorer.rs b/crates/ark/src/data_explorer/r_data_explorer.rs index d0f1db1b5..b3da7b809 100644 --- a/crates/ark/src/data_explorer/r_data_explorer.rs +++ b/crates/ark/src/data_explorer/r_data_explorer.rs @@ -72,6 +72,7 @@ use crossbeam::channel::Sender; use crossbeam::select; use harp::exec::RFunction; use harp::exec::RFunctionExt; +use harp::get_option; use harp::object::RObject; use harp::r_symbol; use harp::table_kind; @@ -1159,6 +1160,7 @@ impl RDataExplorer { }]), }, }, + format_options: Some(Self::current_format_options()), }; Ok(DataExplorerBackendReply::GetStateReply(state)) } @@ -1299,6 +1301,36 @@ impl RDataExplorer { // Call the conversion function with resolved sort keys convert_to_code::convert_to_code(params, object_name, &resolved_sort_keys) } + + /// Returns the current format options as a `FormatOptions` object. + /// + /// These options are derived from the R options "scipen" and "digits". The + /// thresholds for scientific notation are calculated based on these + /// options. The `max_value_length` is set to 1000, and `thousands_sep` + /// is set to `None`. + fn current_format_options() -> FormatOptions { + // In R 4.2, Rf_GetOption1 (used by get_option) doesn't work correctly for scipen + // due to special handling added in later versions. We need to use the R interpreter's + // getOption() function instead. See: https://github.com/wch/r-source/commit/7f20c19 + // Note: 'digits' works fine with Rf_GetOption1, so we don't need to change it. + let scipen: i64 = harp::parse_eval_global("as.integer(getOption('scipen', 0))") + .and_then(|obj| obj.try_into()) + .unwrap_or(0); // R default + let digits: i64 = get_option("digits").try_into().unwrap_or(7); // R default + + // Calculate thresholds for scientific notation + let max_integral_digits = (scipen + 5).max(1); // only depends on scipen + let large_num_digits = (digits - 5).max(0); // default to 2 d.p. + let small_num_digits = (scipen + 6).max(1); // only depends on scipen + + FormatOptions { + large_num_digits, + small_num_digits, + max_integral_digits, + max_value_length: 1000, + thousands_sep: None, + } + } } /// Open an R object in the data viewer. diff --git a/crates/ark/tests/data_explorer.rs b/crates/ark/tests/data_explorer.rs index 2a49ed2d9..d8b76b0d3 100644 --- a/crates/ark/tests/data_explorer.rs +++ b/crates/ark/tests/data_explorer.rs @@ -3108,3 +3108,63 @@ fn test_single_row_data_frame_column_profiles() { }); } } + +#[test] +fn test_format_options_state_change() { + let _lock = r_test_lock(); + + // Save the current R options so we can restore them later + let (original_scipen, original_digits) = r_task(|| { + let scipen_obj = harp::parse_eval_global("getOption('scipen')").expect("Failed to get scipen option"); + let digits_obj = harp::parse_eval_global("getOption('digits')").expect("Failed to get digits option"); + let scipen: i64 = scipen_obj.try_into().unwrap_or(0); + let digits: i64 = digits_obj.try_into().unwrap_or(7); + (scipen, digits) + }); + + // Set known R options for testing + r_task(|| { + harp::parse_eval_global("options(scipen = 0, digits = 7)").unwrap(); + }); + + // Open a data explorer with mtcars + let setup = TestSetup::new("mtcars"); + let socket = setup.socket(); + + // Get the initial state and verify format_options + TestAssertions::assert_state(&socket, |state| { + let format_options = state.format_options.as_ref().expect("format_options should be present"); + + // With scipen=0, digits=7: + // max_integral_digits = (0 + 5).max(1) = 5 + // large_num_digits = (7 - 5).max(0) = 2 + // small_num_digits = (0 + 6).max(1) = 6 + assert_eq!(format_options.max_integral_digits, 5); + assert_eq!(format_options.large_num_digits, 2); + assert_eq!(format_options.small_num_digits, 6); + }); + + // Change R options to different values + r_task(|| { + harp::parse_eval_global("options(scipen = 10, digits = 10)").unwrap(); + }); + + // Get the state again and verify format_options changed + TestAssertions::assert_state(&socket, |state| { + let format_options = state.format_options.as_ref().expect("format_options should be present"); + + // With scipen=10, digits=10: + // max_integral_digits = (10 + 5).max(1) = 15 + // large_num_digits = (10 - 5).max(0) = 5 + // small_num_digits = (10 + 6).max(1) = 16 + assert_eq!(format_options.max_integral_digits, 15); + assert_eq!(format_options.large_num_digits, 5); + assert_eq!(format_options.small_num_digits, 16); + }); + + // Restore original R options + r_task(|| { + let restore_cmd = format!("options(scipen = {}, digits = {})", original_scipen, original_digits); + harp::parse_eval_global(&restore_cmd).expect("Failed to restore original R options"); + }); +}