use runmat_builtins::{
set_display_format, BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor,
BuiltinOutputMode, BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType,
BuiltinSignatureDescriptor, FormatMode, Tensor, Value,
};
use runmat_macros::runtime_builtin;
use crate::{build_runtime_error, BuiltinResult};
const FORMAT_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
name: "ans",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "Empty matrix placeholder returned by sink invocation.",
}];
const FORMAT_INPUTS_NONE: [BuiltinParamDescriptor; 0] = [];
const FORMAT_INPUTS_MODE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
name: "mode",
ty: BuiltinParamType::StringScalar,
arity: BuiltinParamArity::Required,
default: None,
description:
"Format mode keyword (short, long, shortE, longE, shortG, longG, rat, hex, compact, loose).",
}];
const FORMAT_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
BuiltinSignatureDescriptor {
label: "format()",
inputs: &FORMAT_INPUTS_NONE,
outputs: &FORMAT_OUTPUT,
},
BuiltinSignatureDescriptor {
label: "format(mode)",
inputs: &FORMAT_INPUTS_MODE,
outputs: &FORMAT_OUTPUT,
},
];
const FORMAT_ERROR_ARG_CONFIG: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
code: "RM.FORMAT.ARG_CONFIG",
identifier: None,
when: "Input arguments do not match supported format call forms.",
message: "format: invalid argument configuration",
};
const FORMAT_ERROR_UNKNOWN_MODE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
code: "RM.FORMAT.UNKNOWN_MODE",
identifier: None,
when: "Mode string is not a supported numeric display mode keyword.",
message: "format: unknown format mode",
};
const FORMAT_ERRORS: [BuiltinErrorDescriptor; 2] =
[FORMAT_ERROR_ARG_CONFIG, FORMAT_ERROR_UNKNOWN_MODE];
pub const FORMAT_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
signatures: &FORMAT_SIGNATURES,
output_mode: BuiltinOutputMode::Fixed,
completion_policy: BuiltinCompletionPolicy::Public,
errors: &FORMAT_ERRORS,
};
fn format_error_with(
error: &'static BuiltinErrorDescriptor,
message: impl Into<String>,
) -> crate::RuntimeError {
let mut builder = build_runtime_error(message).with_builtin("format");
if let Some(identifier) = error.identifier {
builder = builder.with_identifier(identifier);
}
builder.build()
}
#[runtime_builtin(
name = "format",
category = "io",
summary = "Set numeric display format for console output.",
keywords = "format,display,precision,numeric,short,long,scientific",
sink = true,
suppress_auto_output = true,
descriptor(crate::builtins::io::format::FORMAT_DESCRIPTOR),
builtin_path = "crate::builtins::io::format"
)]
async fn format_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
let keyword = match args.as_slice() {
[] => {
set_display_format(FormatMode::Short);
return Ok(empty_value());
}
[Value::String(s)] => s.to_lowercase(),
[Value::CharArray(ca)] => ca.to_string().to_lowercase(),
_ => {
return Err(format_error_with(
&FORMAT_ERROR_ARG_CONFIG,
"format: unrecognized argument; expected a format name such as 'short' or 'long'",
));
}
};
if matches!(keyword.trim(), "compact" | "loose") {
return Ok(empty_value());
}
set_display_format(parse_numeric_mode(keyword.trim())?);
Ok(empty_value())
}
fn parse_numeric_mode(s: &str) -> BuiltinResult<FormatMode> {
match s {
"short" => Ok(FormatMode::Short),
"long" => Ok(FormatMode::Long),
"shorte" => Ok(FormatMode::ShortE),
"longe" => Ok(FormatMode::LongE),
"shortg" => Ok(FormatMode::ShortG),
"longg" => Ok(FormatMode::LongG),
"rat" | "rational" => Ok(FormatMode::Rational),
"hex" => Ok(FormatMode::Hex),
other => Err(format_error_with(
&FORMAT_ERROR_UNKNOWN_MODE,
format!(
"format: unknown format '{other}'; numeric modes: short, long, shortE, longE, shortG, longG, rat, hex"
),
)),
}
}
fn empty_value() -> Value {
Value::Tensor(Tensor::zeros(vec![0, 0]))
}
#[cfg(test)]
mod tests {
use super::*;
use futures::executor::block_on;
use runmat_builtins::{format_number, get_display_format, FormatMode};
use std::f64::consts::PI;
fn with_format<F: FnOnce()>(mode: FormatMode, f: F) {
let prev = get_display_format();
set_display_format(mode);
f();
set_display_format(prev);
}
#[test]
fn test_parse_numeric_mode_case_insensitive() {
let labels: Vec<&str> = FORMAT_DESCRIPTOR
.signatures
.iter()
.map(|sig| sig.label)
.collect();
assert!(labels.contains(&"format()"));
assert!(labels.contains(&"format(mode)"));
assert_eq!(parse_numeric_mode("short").unwrap(), FormatMode::Short);
assert_eq!(parse_numeric_mode("long").unwrap(), FormatMode::Long);
assert_eq!(parse_numeric_mode("shorte").unwrap(), FormatMode::ShortE);
assert_eq!(parse_numeric_mode("longe").unwrap(), FormatMode::LongE);
assert_eq!(parse_numeric_mode("shortg").unwrap(), FormatMode::ShortG);
assert_eq!(parse_numeric_mode("longg").unwrap(), FormatMode::LongG);
assert_eq!(parse_numeric_mode("rat").unwrap(), FormatMode::Rational);
assert_eq!(
parse_numeric_mode("rational").unwrap(),
FormatMode::Rational
);
assert_eq!(parse_numeric_mode("hex").unwrap(), FormatMode::Hex);
}
#[test]
fn test_parse_numeric_mode_unknown() {
assert!(parse_numeric_mode("bank").is_err());
assert!(parse_numeric_mode("").is_err());
assert!(parse_numeric_mode("compact").is_err());
assert!(parse_numeric_mode("loose").is_err());
}
#[test]
fn test_spacing_modes_do_not_change_numeric_format() {
block_on(async {
format_builtin(vec![Value::String("long".to_string())])
.await
.unwrap();
assert_eq!(get_display_format(), FormatMode::Long);
format_builtin(vec![Value::String("compact".to_string())])
.await
.unwrap();
assert_eq!(
get_display_format(),
FormatMode::Long,
"compact must not reset numeric format"
);
format_builtin(vec![Value::String("loose".to_string())])
.await
.unwrap();
assert_eq!(
get_display_format(),
FormatMode::Long,
"loose must not reset numeric format"
);
set_display_format(FormatMode::Short);
});
}
#[test]
fn test_set_display_format() {
with_format(FormatMode::Long, || {
assert_eq!(get_display_format(), FormatMode::Long);
});
}
#[test]
fn format_builtin_long_sets_mode_and_pi_displays_full_precision() {
block_on(async {
format_builtin(vec![Value::String("long".to_string())])
.await
.unwrap();
assert_eq!(format_number(PI), "3.141592653589793");
set_display_format(FormatMode::Short);
});
}
#[test]
fn format_builtin_short_pi_displays_four_decimal_places() {
block_on(async {
format_builtin(vec![Value::String("short".to_string())])
.await
.unwrap();
assert_eq!(format_number(PI), "3.1416");
assert_eq!(format_number(0.5), "0.5000");
assert_eq!(format_number(1.0), "1");
});
}
#[test]
fn format_builtin_short_e_always_scientific() {
block_on(async {
format_builtin(vec![Value::String("shortE".to_string())])
.await
.unwrap();
assert_eq!(format_number(PI), "3.1416e+00");
assert_eq!(format_number(1000.0), "1.0000e+03");
set_display_format(FormatMode::Short);
});
}
#[test]
fn format_builtin_no_args_resets_to_short() {
block_on(async {
format_builtin(vec![Value::String("long".to_string())])
.await
.unwrap();
format_builtin(vec![]).await.unwrap();
assert_eq!(get_display_format(), FormatMode::Short);
});
}
#[test]
fn format_builtin_rational_pi_approximation() {
block_on(async {
format_builtin(vec![Value::String("rat".to_string())])
.await
.unwrap();
assert_eq!(format_number(PI), "355/113");
set_display_format(FormatMode::Short);
});
}
#[test]
fn format_builtin_hex_pi() {
block_on(async {
format_builtin(vec![Value::String("hex".to_string())])
.await
.unwrap();
assert_eq!(format_number(PI), "400921fb54442d18");
set_display_format(FormatMode::Short);
});
}
#[test]
fn format_builtin_small_value_goes_scientific_in_short() {
block_on(async {
format_builtin(vec![Value::String("short".to_string())])
.await
.unwrap();
assert_eq!(format_number(1e-5), "1.0000e-05");
});
}
}