use super::{JsonError, JsonPhase};
use crate::phase::utils::limit_denominator;
use crate::phase::Phase;
use num::{FromPrimitive, One, Rational64, Zero};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PhaseOptions {
pub ignore_value: Option<Phase>,
pub ignore_approx: bool,
pub ignore_pi: bool,
pub limit_denom: Option<i64>,
}
impl Default for PhaseOptions {
fn default() -> Self {
Self {
ignore_value: None,
ignore_approx: false,
ignore_pi: false,
limit_denom: Some(256),
}
}
}
impl JsonPhase {
pub fn from_phase(phase: impl Into<Phase>, options: PhaseOptions) -> Self {
let phase = phase.into();
if let Some(ignore_value) = options.ignore_value {
if phase == ignore_value {
return Self("".to_string());
}
}
let phase: Rational64 = phase.to_rational();
if phase.is_zero() {
return Self("0".to_string());
}
let mut simstr: &str = "";
let mut phase = phase;
if let Some(limit_denom) = options.limit_denom {
if *phase.denom() > limit_denom {
if !options.ignore_approx {
simstr = "~";
}
phase = limit_denominator(phase, limit_denom);
}
};
let numer = match (options.ignore_pi, *phase.numer()) {
(false, 1) => "pi".to_string(),
(false, -1) => "-pi".to_string(),
(false, n) => format!("{n}*pi"),
(true, n) => format!("{n}"),
};
let denom = match *phase.denom() {
1 => "".to_string(),
d => format!("/{d}"),
};
Self(format!("{simstr}{numer}{denom}"))
}
pub fn to_phase(&self) -> Result<Option<Phase>, JsonError> {
if self.0.is_empty() {
return Ok(None);
}
let phase_error = || JsonError::InvalidPhase {
phase: self.0.clone(),
};
let s: String = self
.0
.chars()
.map(|c| c.to_ascii_lowercase())
.filter(|c| !c.is_whitespace())
.filter(|&c| c != 'π')
.filter(|&c| c != '~')
.collect::<String>();
let s = s.replace(r#"\pi"#, "");
let s = s.replace("pi", "");
let s = s.as_str();
let s = s.trim_start_matches('*').trim_end_matches('*');
if s.is_empty() {
return Ok(Some(Phase::one()));
}
if s == "-" {
return Ok(Some(-Phase::one()));
}
if s.contains('.') || s.contains('e') {
let f: f64 = s.parse().map_err(|_| phase_error())?;
let phase: Phase = f.into();
let phase = phase.limit_denominator(256);
return Ok(Some(phase));
}
if s.contains('/') {
let mut parts = s.split('/');
let num: &str = parts.next().unwrap().trim_end_matches('*');
let den: i64 = parts.next().unwrap().parse().map_err(|_| phase_error())?;
return Ok(Some(
match num {
"" => Rational64::new(1, den),
"-" => Rational64::new(-1, den),
_ => Rational64::new(num.parse().map_err(|_| phase_error())?, den),
}
.into(),
));
}
let n: i64 = s.parse().map_err(|_| phase_error())?;
let r: Rational64 = Rational64::from_i64(n).ok_or_else(phase_error)?;
Ok(Some(r.into()))
}
}
#[cfg(test)]
mod test {
use rstest::rstest;
use super::*;
#[rstest]
#[case(0, "0")]
#[case(1, "pi")]
#[case((1, 2), "pi/2")]
#[case((1, 3), "pi/3")]
#[case((-1, 2), "-pi/2")]
#[case((-1, 3), "-pi/3")]
#[case((2, 3), "2*pi/3")]
fn test_from_phase(#[case] phase: impl Into<Phase>, #[case] expected: &str) {
let phase = phase.into();
let json_phase = JsonPhase::from_phase(phase, Default::default());
assert_eq!(json_phase.0, expected);
}
#[rstest]
#[case("0", 0)]
#[case("1", 1)]
#[case("1/2", (1, 2))]
#[case("1/3", (1, 3))]
#[case("-1/2", (-1, 2))]
#[case("-1", 1)]
#[case("pi", 1)]
#[case("-pi", 1)]
#[case("pi/3", (1, 3))]
#[case("-pi/3", (-1, 3))]
#[case("1/3 * pi", (1, 3))]
#[case("2*pi/3", (2, 3))]
#[case("-0.3333333333333333*pi", (-1, 3))]
#[case("1*π", 1)]
#[case("π", 1)]
#[case("~-pi/2", (-1, 2))]
#[case(r#"7\pi/4"#, (7, 4))]
fn test_to_phase(#[case] s: &str, #[case] expected: impl Into<Phase>) {
let expected = expected.into();
let json_phase = JsonPhase(s.to_string());
let phase = json_phase.to_phase().unwrap().unwrap_or(Phase::zero());
assert_eq!(phase, expected);
}
}