use crate::circuit::Circuit;
use crate::circuit::circuit_param::CircuitParam;
use crate::circuit::parameter::Parameter;
use crate::visualization::VisualizationError;
use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParameterDisplayMode {
Numeric,
Symbolic,
SymbolicWithValue,
PiFractionPreferred,
}
#[derive(Debug, Clone, Copy)]
pub struct ParameterFormatOptions {
pub mode: ParameterDisplayMode,
pub decimal_precision: usize,
pub scientific_lower_bound: f64,
pub scientific_upper_bound: f64,
pub pi_tolerance: f64,
pub pi_max_denominator: i64,
}
impl Default for ParameterFormatOptions {
fn default() -> Self {
Self {
mode: ParameterDisplayMode::Numeric,
decimal_precision: 2,
scientific_lower_bound: 1e-3,
scientific_upper_bound: 1e4,
pi_tolerance: 1e-3,
pi_max_denominator: 16,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ParameterFormatter {
options: ParameterFormatOptions,
}
impl ParameterFormatter {
pub fn new(options: ParameterFormatOptions) -> Self {
Self { options }
}
pub fn format_circuit_param(
&self,
circuit: &Circuit,
param: &CircuitParam,
) -> Result<String, VisualizationError> {
match param {
CircuitParam::Fixed(v) => Ok(self.format_from_expr_and_value(None, Some(*v))),
CircuitParam::Index(idx) => {
let p = circuit.parameters().get_index(*idx as usize).ok_or(
VisualizationError::ParameterIndexOutOfBounds {
index: *idx,
len: circuit.parameters().len(),
},
)?;
Ok(self.format_parameter(p))
}
}
}
fn format_parameter(&self, parameter: &Parameter) -> String {
let expr = parameter.to_string();
let value = parameter.evaluate(&None).ok();
self.format_from_expr_and_value(Some(expr.as_str()), value)
}
fn format_from_expr_and_value(&self, expr: Option<&str>, value: Option<f64>) -> String {
match self.options.mode {
ParameterDisplayMode::Numeric => value
.map(|v| self.format_numeric(v))
.or_else(|| expr.map(|e| self.format_symbolic_expr(e)))
.unwrap_or_else(|| "0".to_string()),
ParameterDisplayMode::Symbolic => expr
.map(|e| self.format_symbolic_expr(e))
.or_else(|| value.map(|v| self.format_numeric(v)))
.unwrap_or_else(|| "0".to_string()),
ParameterDisplayMode::SymbolicWithValue => match (expr, value) {
(Some(e), Some(v)) => {
let expr_str = self.format_symbolic_expr(e);
let value_str = self.format_numeric(v);
if expr_str == value_str {
expr_str
} else {
format!("{expr_str} ≈ {value_str}")
}
}
(Some(e), None) => self.format_symbolic_expr(e),
(None, Some(v)) => self.format_numeric(v),
(None, None) => "0".to_string(),
},
ParameterDisplayMode::PiFractionPreferred => value
.and_then(|v| self.format_pi_fraction(v))
.or_else(|| value.map(|v| self.format_numeric(v)))
.or_else(|| expr.map(|e| self.format_symbolic_expr(e)))
.unwrap_or_else(|| "0".to_string()),
}
}
fn format_symbolic_expr(&self, expr: &str) -> String {
let bytes = expr.as_bytes();
let mut out = String::with_capacity(expr.len());
let mut i = 0usize;
while i < bytes.len() {
if is_number_start(bytes, i)
&& !is_ascii_identifier_byte(previous_byte(bytes, i).unwrap_or_default())
{
let end = scan_number_literal(bytes, i);
let next = bytes.get(end).copied().unwrap_or_default();
if !is_ascii_identifier_byte(next) {
let token = &expr[i..end];
if let Ok(value) = token.parse::<f64>() {
out.push_str(&self.format_numeric(value));
i = end;
continue;
}
}
}
let ch = expr[i..].chars().next().unwrap_or_default();
out.push(ch);
i += ch.len_utf8();
}
out
}
fn format_pi_fraction(&self, value: f64) -> Option<String> {
if !value.is_finite() {
return None;
}
if value == 0.0 {
return Some("0".to_string());
}
let ratio = value / PI;
let max_den = self.options.pi_max_denominator.max(1);
let mut best: Option<(i64, i64, f64)> = None;
for den in 1..=max_den {
let num = (ratio * den as f64).round() as i64;
if num == 0 {
continue;
}
let approx = num as f64 / den as f64;
let err = (ratio - approx).abs();
if err > self.options.pi_tolerance {
continue;
}
let divisor = gcd_i64(num.abs(), den);
let reduced_num = num / divisor;
let reduced_den = den / divisor;
match best {
None => best = Some((reduced_num, reduced_den, err)),
Some((b_num, b_den, b_err)) => {
if err < b_err
|| (err == b_err
&& (reduced_den < b_den
|| (reduced_den == b_den && reduced_num.abs() < b_num.abs())))
{
best = Some((reduced_num, reduced_den, err));
}
}
}
}
let (num, den, _) = best?;
Some(format_pi_ratio(num, den))
}
fn format_numeric(&self, value: f64) -> String {
if value.is_nan() || value.is_infinite() {
return value.to_string();
}
let abs = value.abs();
if abs > 0.0
&& (abs < self.options.scientific_lower_bound
|| abs >= self.options.scientific_upper_bound)
{
return format_scientific(value, self.options.decimal_precision.max(1));
}
let mut s = format!("{:.*}", self.options.decimal_precision, value);
if self.options.decimal_precision > 0 {
s = s.trim_end_matches('0').trim_end_matches('.').to_string();
}
if s == "-0" {
s = "0".to_string();
}
if s == "0" && value != 0.0 {
return format_scientific(value, self.options.decimal_precision.max(1));
}
s
}
}
fn format_pi_ratio(num: i64, den: i64) -> String {
if den == 1 {
return match num {
1 => "π".to_string(),
-1 => "-π".to_string(),
_ => format!("{num}π"),
};
}
match num {
1 => format!("π/{den}"),
-1 => format!("-π/{den}"),
_ => format!("{num}π/{den}"),
}
}
fn format_scientific(value: f64, precision: usize) -> String {
let s = format!("{:.*e}", precision, value);
let (mantissa, exponent) = match s.split_once('e') {
Some(parts) => parts,
None => return s,
};
let mut mantissa = mantissa
.trim_end_matches('0')
.trim_end_matches('.')
.to_string();
if mantissa == "-0" {
mantissa = "0".to_string();
}
let exp_value = exponent.parse::<i32>().unwrap_or(0);
format!("{mantissa}e{exp_value}")
}
fn previous_byte(bytes: &[u8], index: usize) -> Option<u8> {
index.checked_sub(1).and_then(|idx| bytes.get(idx)).copied()
}
fn is_ascii_identifier_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || byte == b'_'
}
fn is_number_start(bytes: &[u8], index: usize) -> bool {
bytes[index].is_ascii_digit()
|| (bytes[index] == b'.' && bytes.get(index + 1).is_some_and(u8::is_ascii_digit))
}
fn scan_number_literal(bytes: &[u8], start: usize) -> usize {
let mut end = start;
while bytes.get(end).is_some_and(u8::is_ascii_digit) {
end += 1;
}
if bytes.get(end) == Some(&b'.') {
end += 1;
while bytes.get(end).is_some_and(u8::is_ascii_digit) {
end += 1;
}
}
if matches!(bytes.get(end), Some(b'e' | b'E')) {
let exp_start = end;
let mut exp_end = end + 1;
if matches!(bytes.get(exp_end), Some(b'+' | b'-')) {
exp_end += 1;
}
let digit_start = exp_end;
while bytes.get(exp_end).is_some_and(u8::is_ascii_digit) {
exp_end += 1;
}
if exp_end > digit_start {
end = exp_end;
} else {
end = exp_start;
}
}
end
}
fn gcd_i64(mut a: i64, mut b: i64) -> i64 {
while b != 0 {
let r = a % b;
a = b;
b = r;
}
a.abs().max(1)
}