use std::fmt::Write;
use itertools::Itertools;
use crate::{
chemistry::{AmbiguousLabel, Element, MassOutputType, MolecularFormula},
glycan::GlycanPosition,
parse_json::{ParseJson, use_serde},
system::{Mass, OrderedMass, Ratio, da, fraction},
};
impl From<&MolecularFormula> for OrderedMass {
fn from(value: &MolecularFormula) -> Self {
value.monoisotopic_mass().into()
}
}
impl MolecularFormula {
#[expect(clippy::missing_panics_doc)]
pub fn monoisotopic_mass(&self) -> Mass {
let mut mass = da(*self.additional_mass);
for (e, i, n) in &self.elements {
mass += e
.mass(*i)
.expect("An invalid molecular formula was created, please report this crash")
* Ratio::new::<fraction>(f64::from(*n));
}
mass
}
#[expect(clippy::missing_panics_doc)]
pub fn average_weight(&self) -> Mass {
let mut mass = da(*self.additional_mass); for (e, i, n) in &self.elements {
mass += e
.average_weight(*i)
.expect("An invalid molecular formula was created, please report this crash")
* Ratio::new::<fraction>(f64::from(*n));
}
mass
}
#[cfg(feature = "isotopes")]
pub fn most_abundant_mass(&self) -> Mass {
let isotopes = self.isotopic_distribution(0.01);
let max = isotopes
.iter()
.enumerate()
.max_by_key(|s| ordered_float::OrderedFloat(*s.1));
self.monoisotopic_mass() + da(max.map_or(0.0, |f| f.0 as f64 * 1.0033548353399997)) }
pub fn hill_notation_core(&self) -> String {
self.hill_notation_generic(
|element, buffer| {
if let Some(isotope) = element.1 {
write!(buffer, "[{}{}{}]", isotope, element.0, element.2).unwrap();
} else {
write!(buffer, "{}{}", element.0, element.2).unwrap();
}
},
"",
false,
false,
)
}
pub fn hill_notation(&self) -> String {
self.hill_notation_generic(
|element, buffer| {
if let Some(isotope) = element.1 {
write!(buffer, "[{}{}{}]", isotope, element.0, element.2).unwrap();
} else {
write!(buffer, "{}{}", element.0, element.2).unwrap();
}
},
"",
true,
true,
)
}
pub fn hill_notation_fancy(&self) -> String {
self.hill_notation_generic(
|element, buffer| {
if let Some(isotope) = element.1 {
write!(buffer, "{}", to_superscript_num(isotope.get())).unwrap();
}
write!(buffer, "{}", element.0).unwrap();
if element.2 != 1 {
write!(buffer, "{}", to_subscript_num(element.2 as isize)).unwrap();
}
},
"",
true,
true,
)
}
pub fn hill_notation_html(&self) -> String {
self.hill_notation_generic(
|element, buffer| {
if let Some(isotope) = element.1 {
write!(buffer, "<sup>{isotope}</sup>").unwrap();
}
write!(buffer, "{}", element.0).unwrap();
if element.2 != 1 {
write!(buffer, "<sub>{}</sub>", element.2).unwrap();
}
},
"",
true,
true,
)
}
pub fn hill_notation_xlmod(&self) -> Option<String> {
if self.charge().value != 0 || self.additional_mass != 0.0 {
None
} else {
Some(self.hill_notation_generic(
|(element, isotope, amount), buffer| {
if *amount < 0 {
write!(buffer, "-").unwrap();
}
if isotope.is_some_and(|i| i.get() == 2) && *element == Element::H {
write!(buffer, "D{}", amount.abs()).unwrap();
} else {
if let Some(isotope) = isotope {
write!(buffer, "{isotope}").unwrap();
}
write!(buffer, "{element}{}", amount.abs()).unwrap();
}
},
" ",
false,
false,
))
}
}
pub fn hill_notation_psi_mod(&self) -> Option<String> {
if self.charge().value != 0 || self.additional_mass != 0.0 {
None
} else {
Some(self.hill_notation_generic(
|(element, isotope, amount), buffer| {
if let Some(isotope) = isotope {
write!(buffer, "({isotope})").unwrap();
}
write!(buffer, "{element} {amount}").unwrap();
},
" ",
false,
false,
))
}
}
}
impl ParseJson for MolecularFormula {
fn from_json_value(
value: serde_json::Value,
) -> Result<Self, context_error::BoxedError<'static, context_error::BasicKind>> {
use_serde(value)
}
}
impl std::fmt::Display for AmbiguousLabel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AminoAcid {
option,
sequence_index,
peptidoform_index,
peptidoform_ion_index,
} => write!(
f,
"{option}@p{}.{}i{}",
peptidoform_ion_index + 1,
peptidoform_index + 1,
sequence_index + 1
),
Self::Modification {
id,
sequence_index,
peptidoform_index,
peptidoform_ion_index,
} => write!(
f,
"\x23{id}@p{}.{}i{}",
peptidoform_ion_index + 1,
peptidoform_index + 1,
sequence_index
),
Self::ChargeCarrier(formula) => write!(f, "[{}]", formula.hill_notation()),
Self::CrossLinkBound(name) => write!(f, "intact{name}"),
Self::CrossLinkBroken(name, formula) => {
write!(f, "broken{name}@{}", formula.hill_notation())
}
Self::GlycanFragment(bonds) => {
write!(f, "Y{}", bonds.iter().map(GlycanPosition::label).join("Y"))
}
Self::GlycanFragmentComposition(composition) => write!(
f,
"Y{}",
composition
.iter()
.map(|(sugar, amount)| format!("{sugar}{amount}"))
.join("")
),
}
}
}
#[expect(clippy::missing_panics_doc)] fn to_subscript_num(input: isize) -> String {
let text = input.to_string();
let mut output = String::new();
for c in text.as_bytes() {
if *c == b'-' {
output.push('\u{208B}');
} else {
output.push(char::from_u32(u32::from(*c) + 0x2080 - 0x30).unwrap());
}
}
output
}
#[expect(clippy::missing_panics_doc)] fn to_superscript_num(input: u16) -> String {
let text = input.to_string();
let mut output = String::new();
for c in text.as_bytes() {
if *c == b'1' {
output.push('\u{00B9}');
} else if *c == b'2' {
output.push('\u{00B2}');
} else if *c == b'3' {
output.push('\u{00B3}');
} else {
output.push(char::from_u32(u32::from(*c) + 0x2070 - 0x30).unwrap());
}
}
output
}
#[cfg(test)]
#[expect(clippy::missing_panics_doc)]
mod tests {
use crate::chemistry::MolecularFormula;
#[test]
fn sorted() {
assert_eq!(molecular_formula!(H 2 O 2), molecular_formula!(O 2 H 2));
assert_eq!(
molecular_formula!(H 6 C 2 O 1),
molecular_formula!(O 1 C 2 H 6)
);
assert_eq!(
molecular_formula!(H 6 C 2 O 1),
molecular_formula!(O 1 H 6 C 2)
);
}
#[test]
fn simplified() {
assert_eq!(molecular_formula!(H 2 O 1 O 1), molecular_formula!(O 2 H 2));
assert_eq!(
molecular_formula!(H 2 O 1 O 1 H 1 H -2 H 0 H -1 H 2),
molecular_formula!(O 2 H 2)
);
assert_eq!(
molecular_formula!(H 2 Sb 0 O 1 O 1 H 1 H -2 H 0 H -1 N 0 P 0 Na 0 H 2),
molecular_formula!(O 2 H 2)
);
}
#[test]
fn add() {
assert_eq!(
molecular_formula!(H 2 O 2),
molecular_formula!(H 1 O 1) + molecular_formula!(H 1 O 1)
);
assert_eq!(
molecular_formula!(H 2 O 2),
molecular_formula!(H 1 O 3) + molecular_formula!(H 1 O -1)
);
assert_eq!(
molecular_formula!(H 2 O 2),
molecular_formula!(H 1 O -1) + molecular_formula!(H 1 O 3)
);
assert_eq!(
molecular_formula!(H 2 O 2),
molecular_formula!(H 1 O -1) + molecular_formula!(O 3 H 1)
);
assert_eq!(
molecular_formula!(H 2 O 2),
molecular_formula!(H 2 O -1) + molecular_formula!(O 3)
);
}
#[test]
fn pro_forma_spaces() {
assert_eq!(
MolecularFormula::pro_forma::<false, false>("C1[13C1]H6"),
MolecularFormula::pro_forma::<false, false>("C 1 [ 13 C 1 ] H 6")
);
}
#[test]
fn pro_forma_empty() {
assert_eq!(
MolecularFormula::pro_forma::<false, true>("(empty)"),
MolecularFormula::pro_forma::<false, true>("H0")
);
assert_eq!(
MolecularFormula::pro_forma::<false, true>(""),
MolecularFormula::pro_forma::<false, true>("H0")
);
assert!(MolecularFormula::pro_forma::<false, false>("").is_err());
assert_eq!(
MolecularFormula::pro_forma::<false, false>("O"),
Ok(molecular_formula!(O 1))
);
}
}