use std::{borrow::Cow, fmt::Display, hash::Hash};
use context_error::*;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use thin_vec::ThinVec;
use crate::{
chemistry::{Chemical, ELEMENT_PARSE_LIST, Element, MolecularFormula},
glycan::lists::*,
helper_functions::str_starts_with,
molecular_formula,
parse_json::{ParseJson, use_serde},
sequence::SequencePosition,
space::{Space, UsedSpace},
};
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum Configuration {
D,
L,
DD,
LL,
DL,
LD,
}
#[derive(Clone, Debug, Deserialize, Ord, PartialOrd, Serialize)]
pub struct MonoSaccharide {
pub(super) base_sugar: BaseSugar,
pub(super) substituents: ThinVec<GlycanSubstituent>,
pub(super) furanose: bool,
pub(super) configuration: Option<Configuration>,
}
impl Space for MonoSaccharide {
fn space(&self) -> UsedSpace {
(self.base_sugar.space()
+ self.substituents.space()
+ self.furanose.space()
+ self.configuration.space())
.set_total::<Self>()
}
}
impl MonoSaccharide {
pub fn equivalent(&self, other: &Self, precise: bool) -> bool {
self.base_sugar.equivalent(&other.base_sugar, precise)
&& self.substituents == other.substituents
&& (!precise
|| (self.furanose == other.furanose && self.configuration == other.configuration))
}
}
impl PartialEq for MonoSaccharide {
fn eq(&self, other: &Self) -> bool {
self.equivalent(other, true)
}
}
impl Eq for MonoSaccharide {}
impl Hash for MonoSaccharide {
fn hash<H: std::hash::Hasher>(&self, hasher: &mut H) {
self.base_sugar.hash(hasher);
self.substituents.hash(hasher);
self.furanose.hash(hasher);
self.configuration.hash(hasher);
}
}
impl Display for MonoSaccharide {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.pro_forma_name())
}
}
impl ParseJson for MonoSaccharide {
fn from_json_value(value: serde_json::Value) -> Result<Self, BoxedError<'static, BasicKind>> {
use_serde(value)
}
}
impl MonoSaccharide {
pub fn pro_forma_name(&self) -> Cow<'static, str> {
match self.base_sugar {
BaseSugar::Sugar
if self.substituents.is_empty()
&& !self.furanose
&& self.configuration.is_none() =>
{
Cow::Borrowed("Sug")
}
BaseSugar::Triose
if self.substituents.is_empty()
&& !self.furanose
&& self.configuration.is_none() =>
{
Cow::Borrowed("Tri")
}
BaseSugar::Tetrose(_)
if self.substituents.is_empty()
&& !self.furanose
&& self.configuration.is_none() =>
{
Cow::Borrowed("Tet")
}
BaseSugar::Pentose(_)
if self.substituents.is_empty()
&& !self.furanose
&& self.configuration.is_none() =>
{
Cow::Borrowed("Pen")
}
BaseSugar::Hexose(isomer) if !self.furanose && self.configuration.is_none() => {
match *self.substituents.as_slice() {
[] => Cow::Borrowed("Hex"),
[GlycanSubstituent::Acid] => Cow::Borrowed("aHex"),
[
GlycanSubstituent::Acid,
GlycanSubstituent::Deoxy,
GlycanSubstituent::Didehydro,
] => Cow::Borrowed("en,aHex"),
[GlycanSubstituent::Deoxy] if isomer == Some(HexoseIsomer::Galactose) => {
Cow::Borrowed("Fuc")
}
[GlycanSubstituent::Deoxy] => Cow::Borrowed("dHex"),
[GlycanSubstituent::NAcetyl, GlycanSubstituent::Sulfate] => {
Cow::Borrowed("HexNAcS")
}
[GlycanSubstituent::Amino, GlycanSubstituent::Sulfate] => {
Cow::Borrowed("HexNS")
}
[GlycanSubstituent::Amino] => Cow::Borrowed("HexN"),
[GlycanSubstituent::NAcetyl] => Cow::Borrowed("HexNAc"),
[GlycanSubstituent::Sulfate] => Cow::Borrowed("HexS"),
[GlycanSubstituent::Phosphate] => Cow::Borrowed("HexP"),
_ => Cow::Owned(format!("{{{}}}", self.formula())),
}
}
BaseSugar::Heptose(_)
if self.substituents.is_empty()
&& !self.furanose
&& self.configuration.is_none() =>
{
Cow::Borrowed("Hep")
}
BaseSugar::Octose
if self.substituents.is_empty()
&& !self.furanose
&& self.configuration.is_none() =>
{
Cow::Borrowed("Oct")
}
BaseSugar::Nonose(None) if !self.furanose && self.configuration.is_none() => {
match *self.substituents.as_slice() {
[] => Cow::Borrowed("Non"),
[
GlycanSubstituent::Acetyl,
GlycanSubstituent::Acid,
GlycanSubstituent::Amino,
] => Cow::Borrowed("NeuAc"),
[
GlycanSubstituent::Acid,
GlycanSubstituent::Amino,
GlycanSubstituent::Glycolyl,
] => Cow::Borrowed("NeuGc"),
[
GlycanSubstituent::Acid,
GlycanSubstituent::Amino,
GlycanSubstituent::Deoxy,
] => Cow::Borrowed("Neu"),
_ => Cow::Owned(format!("{{{}}}", self.formula())),
}
}
BaseSugar::Decose
if self.substituents.is_empty()
&& !self.furanose
&& self.configuration.is_none() =>
{
Cow::Borrowed("Dec")
}
BaseSugar::Custom(_)
if self.substituents == [GlycanSubstituent::Phosphate]
&& !self.furanose
&& self.configuration.is_none() =>
{
Cow::Borrowed("Phosphate")
}
BaseSugar::Custom(_)
if self.substituents == [GlycanSubstituent::Sulfate]
&& !self.furanose
&& self.configuration.is_none() =>
{
Cow::Borrowed("Sulfate")
}
_ => Cow::Owned(format!("{{{}}}", self.formula())),
}
}
pub const fn base_sugar(&self) -> &BaseSugar {
&self.base_sugar
}
pub fn is_fucose(&self) -> bool {
self.base_sugar == BaseSugar::Hexose(Some(HexoseIsomer::Galactose))
&& self.substituents.contains(&GlycanSubstituent::Deoxy)
}
pub fn new(sugar: BaseSugar, substituents: &[GlycanSubstituent]) -> Self {
Self {
base_sugar: sugar,
substituents: substituents.iter().copied().sorted().collect(),
furanose: false,
configuration: None,
}
}
#[must_use]
pub fn furanose(self) -> Self {
Self {
furanose: true,
..self
}
}
#[must_use]
pub fn configuration(self, configuration: Configuration) -> Self {
Self {
configuration: Some(configuration),
..self
}
}
pub fn from_short_iupac(
original_line: &str,
start: usize,
line_index: u32,
) -> Result<(Self, usize), BoxedError<'_, BasicKind>> {
let mut index = start;
let line = original_line.to_ascii_lowercase();
let bytes = line.as_bytes();
let mut substituents = Vec::new();
let mut configuration = None;
let mut epi = false;
index += line[index..].ignore(&["keto-"]);
if line[index..].starts_with("d-") {
configuration = Some(Configuration::D);
index += 2;
} else if line[index..].starts_with("l-") {
configuration = Some(Configuration::L);
index += 2;
} else if line[index..].starts_with("?-") {
configuration = None;
index += 2;
}
let mut amount = 1;
if bytes[index].is_ascii_digit() {
match bytes.get(index + 1) {
Some(b',') if bytes.get(index + 3).copied() == Some(b':') => {
let start_index = index;
index += 7;
index += line[index..].ignore(&["-"]);
if !line[index..].starts_with("anhydro") {
return Err(BoxedError::new(
BasicKind::Error,
"Invalid iupac monosaccharide name",
"This internally linked glycan could not be parsed, expected Anhydro as modification",
Context::line(
Some(line_index),
original_line,
start_index,
index - start_index + 5,
),
));
}
index += 7;
substituents.extend_from_slice(&[
GlycanSubstituent::Didehydro,
GlycanSubstituent::Deoxy,
GlycanSubstituent::Deoxy,
]);
}
Some(b',') => {
let num = bytes[index + 1..]
.iter()
.take_while(|c| c.is_ascii_digit() || **c == b',' || **c == b'?')
.count();
index += num + 1;
amount = num / 2;
}
Some(_) => index += 1, None => (),
}
index += line[index..].ignore(&["-"]);
}
if line[index..].starts_with('e') {
epi = true;
index += 1;
}
if !line[index..].starts_with("dig") && !line[index..].starts_with("dha") {
if let Some(o) = line[index..].take_any(PREFIX_SUBSTITUENTS, |e| {
substituents.extend(std::iter::repeat_n(*e, amount));
}) {
index += o;
}
index += line[index..].ignore(&["-"]);
}
if line[index..].starts_with("d-") {
configuration = Some(Configuration::D);
index += 2;
} else if line[index..].starts_with("l-") {
configuration = Some(Configuration::L);
index += 2;
} else if line[index..].starts_with("?-") {
configuration = None;
index += 2;
}
let mut sugar = None;
for sug in BASE_SUGARS {
if line[index..].starts_with(sug.0) {
index += sug.0.len();
sugar = Some((sug.1.clone(), sug.2));
break;
}
}
let mut sugar = sugar
.map(|(b, s)| {
let mut alo = Self {
base_sugar: match b {
BaseSugar::Nonose(Some(NonoseIsomer::Leg)) if epi => {
BaseSugar::Nonose(Some(NonoseIsomer::ELeg))
}
other => other,
},
substituents: substituents.into(),
furanose: false,
configuration,
};
alo.substituents.extend(s.iter().copied());
alo
})
.ok_or_else(|| {
BoxedError::new(
BasicKind::Error,
"Invalid iupac monosaccharide name",
"This name could not be recognised as a standard iupac glycan name",
Context::line(Some(line_index), original_line, index, 3),
)
})?;
if index < bytes.len() && bytes[index] == b'f' {
index += 1;
sugar.furanose = true;
}
while index < bytes.len() {
index += line[index..].ignore(&["-"]);
let mut single_amount = 0;
let mut double_amount = 0;
let (offset, mut amount, mut double) = line[index..].parse_location();
index += offset;
if double {
double_amount = amount;
} else {
single_amount = amount;
}
if bytes[index] == b':' {
let (offset, amt, dbl) = line[index + 1..].parse_location();
index += offset + 1;
amount += amt;
if double {
double_amount += amount;
} else {
single_amount += amount;
}
double |= dbl; }
index += line[index..].ignore(&["-"]);
index += line[index..].ignore(&["(x)", "(r)", "(s)"]);
if double {
if let Some(o) = line[index..].take_any(DOUBLE_LINKED_POSTFIX_SUBSTITUENTS, |e| {
sugar.substituents.extend(
e.iter()
.flat_map(|s| std::iter::repeat_n(s, double_amount))
.copied(),
);
if single_amount > 0 {
sugar.substituents.extend(
e.iter()
.filter(|s| **s != GlycanSubstituent::Water)
.flat_map(|s| std::iter::repeat_n(s, single_amount))
.copied(),
);
}
}) {
index += o;
} else {
return Err(BoxedError::new(
BasicKind::Error,
"Invalid iupac monosaccharide name",
"No detected double linked glycan substituent was found, while the pattern for location is for a double linked substituent",
Context::line(Some(line_index), original_line, index, 2),
));
}
} else {
if let Some(o) = line[index..].take_any(POSTFIX_SUBSTITUENTS, |e| {
sugar.substituents.extend(std::iter::repeat_n(*e, amount));
}) {
index += o;
} else if let Some(o) = line[index..].take_any(ELEMENT_PARSE_LIST, |e| {
sugar
.substituents
.extend(std::iter::repeat_n(GlycanSubstituent::Element(*e), amount));
}) {
index += o;
} else {
break;
}
}
if amount != 1 {
index += 1;
}
}
index += line[index..].ignore(&["?"]); sugar.substituents.sort();
Ok((sugar, index))
}
}
trait ParseHelper {
fn ignore(self, ignore: &[&str]) -> usize;
fn take_any<T>(self, parse_list: &[(&str, T)], f: impl FnMut(&T)) -> Option<usize>;
fn parse_location(self) -> (usize, usize, bool);
}
impl ParseHelper for &str {
fn ignore(self, ignore: &[&str]) -> usize {
for i in ignore {
if self.starts_with(i) {
return i.len();
}
}
0
}
fn take_any<T>(self, parse_list: &[(&str, T)], mut f: impl FnMut(&T)) -> Option<usize> {
let mut found = None;
for element in parse_list {
if str_starts_with::<true>(self, element.0) {
found = Some(element.0.len());
f(&element.1);
break;
}
}
found
}
fn parse_location(self) -> (usize, usize, bool) {
let bytes = self.as_bytes();
let mut index = 0;
let mut amount = 1;
let mut double = false;
let possibly_unknown_number = |n: u8| n.is_ascii_digit() || n == b'?';
let number_or_slash = |n: &u8| n.is_ascii_digit() || *n == b'/';
let possibly_unknown_number_or_comma = |n: &u8| possibly_unknown_number(*n) || *n == b',';
if possibly_unknown_number(bytes[0]) && bytes.len() > 1 {
match bytes[1] {
b',' => {
let num = bytes[1..]
.iter()
.copied()
.take_while(possibly_unknown_number_or_comma)
.count();
index += num + 1;
amount = num / 2 + 1;
}
b'-' if bytes[index] != b'?' => {
index += 7;
double = true;
} b'/' => {
let num = bytes[2..]
.iter()
.copied()
.take_while(number_or_slash)
.count();
index += num + 2;
}
c if possibly_unknown_number(c) && bytes[0] == b'?' => {
if bytes[2] == b',' {
let num = bytes[2..]
.iter()
.copied()
.take_while(possibly_unknown_number_or_comma)
.count();
index += num + 2;
amount = num / 2 + 1;
} else if bytes[2] == b'/' {
let num = bytes[3..]
.iter()
.copied()
.take_while(number_or_slash)
.count();
index += num + 3;
} else {
index += 2; }
}
_ => index += 1, }
}
(index, amount, double)
}
}
impl Chemical for MonoSaccharide {
fn formula_inner(
&self,
sequence_index: SequencePosition,
peptidoform_index: usize,
) -> MolecularFormula {
self.base_sugar
.formula_inner(sequence_index, peptidoform_index)
+ self
.substituents
.as_slice()
.formula_inner(sequence_index, peptidoform_index)
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum BaseSugar {
Custom(Box<MolecularFormula>),
Sugar,
Triose,
Tetrose(Option<TetroseIsomer>),
Pentose(Option<PentoseIsomer>),
Hexose(Option<HexoseIsomer>),
Heptose(Option<HeptoseIsomer>),
Octose,
Nonose(Option<NonoseIsomer>),
Decose,
}
impl Space for BaseSugar {
fn space(&self) -> UsedSpace {
(UsedSpace::stack(1)
+ match self {
Self::Custom(f) => f.space(),
Self::Tetrose(i) => i.space(),
Self::Pentose(i) => i.space(),
Self::Hexose(i) => i.space(),
Self::Heptose(i) => i.space(),
Self::Nonose(i) => i.space(),
_ => UsedSpace::default(),
})
.set_total::<Self>()
}
}
impl BaseSugar {
pub fn equivalent(&self, other: &Self, precise: bool) -> bool {
match (self, other) {
(Self::Sugar, Self::Sugar)
| (Self::Octose, Self::Octose)
| (Self::Decose, Self::Decose)
| (Self::Triose, Self::Triose) => true,
(Self::Tetrose(a), Self::Tetrose(b)) => !precise || a == b,
(Self::Pentose(a), Self::Pentose(b)) => !precise || a == b,
(Self::Hexose(a), Self::Hexose(b)) => !precise || a == b,
(Self::Heptose(a), Self::Heptose(b)) => !precise || a == b,
(Self::Nonose(a), Self::Nonose(b)) => !precise || a == b,
(Self::Custom(a), Self::Custom(b)) => a == b,
_ => false,
}
}
}
impl Display for BaseSugar {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Custom(a) => format!("{{{a}}}"),
Self::Sugar => "Sug".to_string(),
Self::Triose => "Tri".to_string(),
Self::Tetrose(_) => "Tet".to_string(),
Self::Pentose(_) => "Pen".to_string(),
Self::Hexose(_) => "Hex".to_string(),
Self::Heptose(_) => "Hep".to_string(),
Self::Octose => "Oct".to_string(),
Self::Nonose(_) => "Non".to_string(),
Self::Decose => "Dec".to_string(),
}
)
}
}
impl Chemical for BaseSugar {
fn formula_inner(
&self,
_sequence_index: SequencePosition,
_peptidoform_index: usize,
) -> MolecularFormula {
match self {
Self::Custom(a) => a.as_ref().clone(),
Self::Sugar => molecular_formula!(H 2 C 2 O 1),
Self::Triose => molecular_formula!(H 4 C 3 O 2),
Self::Tetrose(_) => molecular_formula!(H 6 C 4 O 3),
Self::Pentose(_) => molecular_formula!(H 8 C 5 O 4),
Self::Hexose(_) => molecular_formula!(H 10 C 6 O 5),
Self::Heptose(_) => molecular_formula!(H 12 C 7 O 6),
Self::Octose => molecular_formula!(H 14 C 8 O 7),
Self::Nonose(_) => molecular_formula!(H 16 C 9 O 8),
Self::Decose => molecular_formula!(H 18 C 10 O 9),
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum TetroseIsomer {
Erythrose,
Threose,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum PentoseIsomer {
Ribose,
Arabinose,
Xylose,
Lyxose,
Xylulose,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum HexoseIsomer {
Glucose,
Galactose,
Mannose,
Allose,
Altrose,
Gulose,
Idose,
Talose,
Psicose,
Fructose,
Sorbose,
Tagatose,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum HeptoseIsomer {
GlyceroMannoHeptopyranose, Sedoheptulose,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum NonoseIsomer {
Kdn,
Pse,
Leg,
ELeg,
Aci,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum GlycanSubstituent {
Acetimidoyl,
Acetyl,
Acid,
Alanyl,
Alcohol,
Amino,
Aric,
CargoxyEthylidene,
Deoxy,
DiMethyl,
Didehydro,
Element(Element),
Ethanolamine,
EtOH,
Formyl,
Glyceryl,
Glycolyl,
Glycyl,
HydroxyButyryl,
HydroxyMethyl,
Lac,
Lactyl,
Methyl,
NAcetyl,
NDiMe,
NFo,
NGlycolyl,
OCarboxyEthyl,
PCholine,
Phosphate,
Pyruvyl,
Suc,
Sulfate,
Tauryl,
Ulo,
Ulof,
Water,
}
impl GlycanSubstituent {
pub const fn notation(self) -> &'static str {
match self {
Self::Acetimidoyl => "Am",
Self::Acetyl => "Ac",
Self::Acid => "A",
Self::Alanyl => "Ala",
Self::Alcohol => "ol",
Self::Amino => "N",
Self::Aric => "aric",
Self::CargoxyEthylidene => "Pyr",
Self::Deoxy => "d",
Self::Didehydro => "en",
Self::DiMethyl => "Me2",
Self::Ethanolamine => "Etn",
Self::Element(el) => el.symbol(),
Self::EtOH => "EtOH",
Self::Formyl => "Fo",
Self::Glyceryl => "Gr",
Self::Glycolyl => "Gc",
Self::Glycyl => "Gly",
Self::HydroxyButyryl => "Hb",
Self::HydroxyMethyl => "HMe",
Self::Lac => "Lac",
Self::Lactyl => "Lt",
Self::Methyl => "Me",
Self::NAcetyl => "NAc",
Self::NDiMe => "NDiMe",
Self::NFo => "NFo",
Self::NGlycolyl => "NGc",
Self::OCarboxyEthyl => "carboxyethyl",
Self::PCholine => "PCho",
Self::Phosphate => "P",
Self::Pyruvyl => "Py",
Self::Suc => "Suc",
Self::Sulfate => "S",
Self::Tauryl => "Tau",
Self::Ulo => "ulo",
Self::Ulof => "ulof",
Self::Water => "water_loss",
}
}
}
impl Display for GlycanSubstituent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.notation())
}
}
impl Chemical for GlycanSubstituent {
fn formula_inner(
&self,
_sequence_index: SequencePosition,
_peptidoform_index: usize,
) -> MolecularFormula {
let side = match self {
Self::Acetimidoyl => molecular_formula!(H 5 C 2 N 1),
Self::Acetyl => molecular_formula!(H 3 C 2 O 1),
Self::Acid => molecular_formula!(H -1 O 2), Self::Alanyl => molecular_formula!(H 6 C 3 N 1 O 1),
Self::Alcohol => molecular_formula!(H 3 O 1), Self::Amino => molecular_formula!(H 2 N 1),
Self::Aric => molecular_formula!(H 3 O 3), Self::CargoxyEthylidene => molecular_formula!(H 3 C 3 O 3), Self::Deoxy => molecular_formula!(H 1), Self::Didehydro => molecular_formula!(H -1 O 1), Self::DiMethyl => molecular_formula!(H 5 C 2), Self::Ethanolamine => molecular_formula!(H 6 C 2 N 1 O 1),
Self::EtOH => molecular_formula!(H 5 C 2 O 2),
Self::Element(el) => MolecularFormula::new(&[(*el, None, 1)], &[]).unwrap(),
Self::Formyl => molecular_formula!(H 1 C 1 O 1),
Self::Glyceryl | Self::Lac => molecular_formula!(H 5 C 3 O 3),
Self::Glycolyl => molecular_formula!(H 3 C 2 O 2),
Self::Glycyl | Self::NAcetyl => molecular_formula!(H 4 C 2 N 1 O 1),
Self::HydroxyButyryl => molecular_formula!(H 7 C 4 O 2),
Self::HydroxyMethyl | Self::Ulo => molecular_formula!(H 3 C 1 O 2), Self::Lactyl => molecular_formula!(H 5 C 3 O 2),
Self::Methyl => molecular_formula!(H 3 C 1),
Self::NDiMe => molecular_formula!(H 6 C 2 N 1),
Self::NFo => molecular_formula!(H 2 C 1 N 1 O 1),
Self::NGlycolyl => molecular_formula!(H 4 C 2 N 1 O 2),
Self::OCarboxyEthyl => molecular_formula!(H 6 C 3 O 3), Self::PCholine => molecular_formula!(H 14 C 5 N 1 O 4 P 1),
Self::Phosphate => molecular_formula!(H 2 O 4 P 1),
Self::Pyruvyl => molecular_formula!(H 3 C 3 O 2),
Self::Suc => molecular_formula!(H 6 C 4 N 1 O 3),
Self::Sulfate => molecular_formula!(H 1 O 4 S 1),
Self::Tauryl => molecular_formula!(H 6 C 2 N 1 O 3 S 1),
Self::Ulof => molecular_formula!(H 4 C 1 O 2), Self::Water => molecular_formula!(H - 1),
};
side - molecular_formula!(O 1 H 1) }
}
#[test]
#[allow(clippy::missing_panics_doc)]
fn msfragger_composition() {
let (proforma, _) =
MonoSaccharide::pro_forma_composition::<true>("HexNAc4Hex5Fuc1NeuAc2").unwrap();
let byonic = MonoSaccharide::byonic_composition("HexNAc(4)Hex(5)Fuc(1)NeuAc(2)").unwrap();
assert_eq!(proforma, byonic);
let (proforma, _) = MonoSaccharide::pro_forma_composition::<true>("HexNAc4Hex5").unwrap();
let byonic = MonoSaccharide::byonic_composition("HexNAc(4)Hex(5)").unwrap();
assert_eq!(proforma, byonic);
}