From 047033b2df12e47eecdbc238ea5ba820c342f00f Mon Sep 17 00:00:00 2001 From: Gavin Tran Date: Mon, 20 Mar 2023 23:24:55 -0400 Subject: [PATCH] Add branch recognition (#15) --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/chain.rs | 47 ++++++++++++- src/groups.rs | 103 +++++++++++++++++++++------- src/macros.rs | 9 +-- src/main.rs | 2 +- src/molecule.rs | 156 ++++++++++++++++++++++++++---------------- src/naming.rs | 168 +++++++++++++++++++++++++--------------------- src/spatial.rs | 2 +- src/validation.rs | 43 +++++++++++- 10 files changed, 358 insertions(+), 176 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 338bb70..3d8dd46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,7 +28,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chemcreator" -version = "1.1.0" +version = "1.2.0" dependencies = [ "ruscii", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 9612997..7693433 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chemcreator" -version = "1.1.0" +version = "1.2.0" description = "A text-based tool for identifying organic molecules." authors = ["Gavin Tran"] readme = "README.md" diff --git a/src/chain.rs b/src/chain.rs index ca3dd6f..4894c25 100644 --- a/src/chain.rs +++ b/src/chain.rs @@ -36,7 +36,7 @@ pub(crate) fn get_all_chains(graph: &GridState) -> Fallible>> { let mut out: Vec> = vec![]; for endpoint in endpoints { - out.extend(endpoint_head_chains(endpoint.to_owned(), graph)?); + out.extend(endpoint_head_chains(endpoint.to_owned(), graph, None)?); } Ok(out) } @@ -46,10 +46,14 @@ pub(crate) fn get_all_chains(graph: &GridState) -> Fallible>> { /// ## Errors /// /// Returns [`InvalidGraphError`] if any invalid structures are found while traversing the graph. -fn endpoint_head_chains(endpoint: Atom, graph: &GridState) -> Fallible>> { +pub(crate) fn endpoint_head_chains( + endpoint: Atom, + graph: &GridState, + previous_pos: Option, +) -> Fallible>> { let mut accumulator = vec![vec![]]; - accumulate_carbons(endpoint.pos, None, 0usize, &mut accumulator, graph)?; + accumulate_carbons(endpoint.pos, previous_pos, 0usize, &mut accumulator, graph)?; Ok(accumulator) } @@ -230,6 +234,7 @@ mod tests { pos: Vec2::zero(), }, &graph, + None, ) .unwrap(); @@ -239,6 +244,42 @@ mod tests { assert_ne!(accumulator[0], accumulator[1]); } + #[test] + fn endpoint_head_chains_ignores_previous() { + let graph = graph_with!(5, 3, + [0, 0; A(C)], + [1, 0; A(C)], + [2, 0; A(C)], [2, 1; A(C)], [2, 2; A(C)], + [3, 0; A(C)], + [4, 0; A(C)] + ); + let accumulator = endpoint_head_chains( + Atom { + element: C, + pos: Vec2::xy(1, 0), + }, + &graph, + Some(Vec2::zero()), + ) + .unwrap(); + + assert_eq!(accumulator.len(), 2); + assert_eq!(accumulator[0].len(), 4); + assert_eq!(accumulator[1].len(), 4); + assert_ne!(accumulator[0], accumulator[1]); + assert!(!accumulator + .into_iter() + .flatten() + .collect::>() + .contains({ + if let Cell::Atom(it) = graph.get(Vec2::zero()).unwrap() { + it + } else { + panic!("") + } + })); + } + #[test] fn create_branches_copies_correctly() { let atom = Atom { diff --git a/src/groups.rs b/src/groups.rs index e84b393..b952ea3 100644 --- a/src/groups.rs +++ b/src/groups.rs @@ -3,6 +3,7 @@ //! The `groups` module provides functionality for identifying functional groups on a branch. use crate::chain; +use crate::chain::{endpoint_head_chains, longest_chain}; use crate::groups::InvalidGraphError::{Other, UnrecognizedGroup}; use crate::molecule::Group::{ Alkene, Alkyne, Bromo, Carbonyl, Carboxyl, Chloro, Fluoro, Hydroxyl, Iodo, @@ -15,37 +16,64 @@ use thiserror::Error; /// Generates a [`Branch`] from the given `chain` containing all functional groups attached /// to it. -pub(crate) fn link_groups(graph: &GridState, chain: Vec) -> Fallible { +pub(crate) fn link_groups( + graph: &GridState, + chain: Vec, + parent: Option, +) -> Fallible { let mut branch = Branch::new(chain); fn accumulate_groups( graph: &GridState, accumulator: &mut Branch, index: usize, + parent: Option, ) -> Fallible<()> { if index >= accumulator.chain.len() { return Ok(()); } - let group_nodes = group_directions(graph, accumulator, index)? - .iter() - .map(|&dir| group_node_tree(graph, accumulator.chain[index].pos, dir)) + let directions = group_directions(graph, accumulator, index, parent.clone())?; + + let group_nodes = directions + .0 + .into_iter() + .map(|dir| group_node_tree(graph, accumulator.chain[index].pos, dir)) .collect::>>()?; - let groups = convert_nodes(group_nodes)?; + let mut chain_nodes = directions + .1 + .into_iter() + .map(|dir| branch_node_tree(graph, accumulator.chain[index].pos, dir).unwrap()) + .map(|chain| { + link_groups( + graph, + chain, + Some(Atom { + element: Element::C, + pos: accumulator.chain[index].pos, + }), + ) + .unwrap() + }) + .map(Substituent::Branch) + .collect::>(); + + let mut groups = convert_nodes(group_nodes)?; + groups.append(&mut chain_nodes); accumulator.groups.push(groups); - accumulate_groups(graph, accumulator, index + 1) + accumulate_groups(graph, accumulator, index + 1, parent) } - accumulate_groups(graph, &mut branch, 0usize)?; + accumulate_groups(graph, &mut branch, 0usize, parent)?; Ok(branch) } pub(crate) fn debug_branches(graph: &GridState) -> Fallible { let all_chains = chain::get_all_chains(graph)?; let chain = chain::longest_chain(all_chains)?; - link_groups(graph, chain) + link_groups(graph, chain, None) } /// Converts and combines the given `group_nodes` into [`Substituent`]s. @@ -143,6 +171,16 @@ pub(crate) fn group_node_tree( }) } +pub(crate) fn branch_node_tree( + graph: &GridState, + pos: Vec2, + direction: Direction, +) -> Fallible> { + let atom = Pointer::new(graph, pos).traverse_bond(direction)?; + let chains = endpoint_head_chains(atom, graph, Some(pos))?; + longest_chain(chains) +} + /// Returns a [`Vec`] of [`Direction`] from the [`Atom`] at the given `pos` to bonded atoms /// not including the one at the `previous_pos`. /// @@ -174,7 +212,7 @@ fn next_directions(graph: &GridState, pos: Vec2, previous_pos: Vec2) -> Fallible } /// Returns a [`Vec`] of [`Direction`]s from the [`Atom`] at the given `index` to functional -/// groups. +/// groups and side chains, respectively. /// /// ## Errors /// @@ -184,17 +222,19 @@ fn group_directions( graph: &GridState, accumulator: &Branch, index: usize, -) -> Fallible> { + parent: Option, +) -> Fallible<(Vec, Vec)> { let ptr = Pointer::new(graph, accumulator.chain[index].pos); let directions = ptr .connected_directions() .into_iter() .filter(|&direction| { - let first_element = ptr.traverse_bond(direction).unwrap().element; + let opposite_atom = ptr.traverse_bond(direction).unwrap(); let single_bond = matches!(ptr.bond_order(direction).unwrap(), BondOrder::Single); - let hydrocarbon = - matches!(first_element, Element::C) || matches!(first_element, Element::H); - !single_bond || !hydrocarbon + let hydrogen = matches!(opposite_atom.element, Element::H); + let in_chain = accumulator.chain.contains(&opposite_atom); + let parent = Some(opposite_atom) == parent; + !(hydrogen || parent || in_chain && single_bond) }) .filter(|&direction| { if index > 0 { @@ -208,7 +248,12 @@ fn group_directions( true } }) - .collect::>(); + .partition(|&direction| { + let opposite_atom = ptr.traverse_bond(direction).unwrap(); + let carbon = matches!(opposite_atom.element, Element::C); + let in_chain = accumulator.chain.contains(&opposite_atom); + in_chain || !carbon + }); Ok(directions) } @@ -266,13 +311,14 @@ mod tests { pos: Vec2::xy(3, 1), }, ]; - let branch = link_groups(&graph, chain.clone()).unwrap(); + let branch = link_groups(&graph, chain.clone(), None).unwrap(); let expected = Branch { chain, groups: vec![ vec![Substituent::Group(Chloro)], vec![Substituent::Group(Hydroxyl)], ], + parent_alpha: None, }; assert_eq!(branch, expected); @@ -418,15 +464,23 @@ mod tests { [2, 0; A(C)], [2, 1; B(Double)], [2, 2; A(C)], [2, 3; A(O)], [2, 4; A(H)] ); let branch = Branch { - chain: vec![Atom { - element: C, - pos: Vec2::xy(2, 2), - }], + chain: vec![ + Atom { + element: C, + pos: Vec2::xy(0, 2), + }, + Atom { + element: C, + pos: Vec2::xy(2, 2), + }, + ], groups: vec![], + parent_alpha: None, }; - let directions = group_directions(&graph, &branch, 0usize).unwrap(); + let directions = group_directions(&graph, &branch, 1usize, None).unwrap(); - assert_eq!(directions, vec![Direction::Up, Direction::Down]); + assert_eq!(directions.0, vec![Direction::Up]); + assert_eq!(directions.1, vec![Direction::Down]); } #[test] @@ -444,9 +498,10 @@ mod tests { pos: Vec2::xy(1, 1), }], groups: vec![], + parent_alpha: None, }; - let directions = group_directions(&graph, &branch, 0usize).unwrap(); + let directions = group_directions(&graph, &branch, 0usize, None).unwrap(); - assert_eq!(directions, Direction::all()); + assert_eq!(directions.0, Direction::all()); } } diff --git a/src/macros.rs b/src/macros.rs index 173e14c..fc401bc 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -3,11 +3,10 @@ //! Not to be confused with Rust's `macro_rules!` declarations, the `macros` module contains //! common actions that should be automatically performed for the user when they make an input. -use crate::molecule::BondOrder::Single; use crate::molecule::ComponentType; use crate::molecule::Element::{C, H}; use crate::pointer::Pointer; -use crate::spatial::{EnumAll, GridState, ToVec2}; +use crate::spatial::{EnumAll, GridState}; use ruscii::spatial::Direction; pub fn invoke_macro(graph: &mut GridState) { @@ -31,13 +30,9 @@ pub fn fill_hydrogen(graph: &mut GridState) { fn hydrogen_arm(graph: &mut GridState, direction: Direction) { let mut ptr = Pointer::new(graph, graph.cursor); let first_neighbor = ptr.move_ptr(direction) && !ptr.borrow().unwrap().is_empty(); - let second_neighbor = ptr.move_ptr(direction) && !ptr.borrow().unwrap().is_empty(); let pos = ptr.pos; - if first_neighbor && second_neighbor { - graph.put(pos, ComponentType::Element(H)); - graph.put(pos - direction.to_vec2(), ComponentType::Order(Single)); - } else if first_neighbor && !second_neighbor { + if first_neighbor { graph.put(pos, ComponentType::Element(H)); } } diff --git a/src/main.rs b/src/main.rs index eed53c2..fb987fa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,7 +29,7 @@ mod validation; fn main() { let mut app = App::new(); let version = env!("CARGO_PKG_VERSION"); - let mut graph = GridState::new(20, 10); + let mut graph = GridState::new(21, 11); let mut state = AppState::default(); app.run(|app_state: &mut State, window: &mut Window| { diff --git a/src/molecule.rs b/src/molecule.rs index 30db5e0..f886ce2 100644 --- a/src/molecule.rs +++ b/src/molecule.rs @@ -5,6 +5,7 @@ use crate::molecule::BondOrder::{Double, Single, Triple}; use crate::molecule::BondOrientation::{Horiz, Vert}; use crate::molecule::Element::{C, H, N, O}; +use crate::naming::process_name; use ruscii::spatial::{Direction, Vec2}; use ruscii::terminal::Color; use ruscii::terminal::Color::{Blue, Green, LightGrey, Magenta, Red, White, Xterm, Yellow}; @@ -19,20 +20,6 @@ pub enum Group { Alkane, Alkene, Alkyne, - /* Alkyl groups */ - Methyl, - Ethyl, - Propyl, - Isopropyl, - Butyl, - Pentyl, - Hexyl, - Heptyl, - Octyl, - Nonyl, - Decyl, - /* Alkenyl groups in future */ - /* Alkynyl groups in future */ /* Halide groups */ Bromo, Chloro, @@ -42,7 +29,8 @@ pub enum Group { Hydroxyl, Carbonyl, Carboxyl, - /* Phenyl later */ Ester, + /* Phenyl later */ + Ester, Ether, } @@ -56,17 +44,6 @@ impl Group { pub const fn priority(self) -> Option { let priority = match self { Group::Alkane | Group::Alkene | Group::Alkyne => 0, - Group::Methyl - | Group::Ethyl - | Group::Propyl - | Group::Isopropyl - | Group::Butyl - | Group::Pentyl - | Group::Hexyl - | Group::Heptyl - | Group::Octyl - | Group::Nonyl - | Group::Decyl => return None, Group::Bromo | Group::Chloro | Group::Fluoro | Group::Iodo => return None, Group::Hydroxyl => 3, Group::Carbonyl => 4, @@ -89,17 +66,6 @@ impl Display for Group { Group::Alkane => "an", Group::Alkene => "en", Group::Alkyne => "yn", - Group::Methyl => "methyl", - Group::Ethyl => "ethyl", - Group::Propyl => "propyl", - Group::Isopropyl => "isopropyl", - Group::Butyl => "butyl", - Group::Pentyl => "pentyl", - Group::Hexyl => "hexyl", - Group::Heptyl => "heptyl", - Group::Octyl => "octyl", - Group::Nonyl => "nonyl", - Group::Decyl => "decyl", Group::Bromo => "bromo", Group::Chloro => "chloro", Group::Fluoro => "fluoro", @@ -362,23 +328,40 @@ pub enum Substituent { Group(Group), } +impl Substituent { + pub fn priority(&self) -> Option { + match self { + Substituent::Group(it) => it.priority(), + Substituent::Branch(_) => None, + } + } + + pub fn is_chain_group(&self) -> bool { + match self { + Substituent::Group(it) => it.is_chain_group(), + Substituent::Branch(_) => false, + } + } +} + impl Display for Substituent { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { write!( f, "{}", match self { - Substituent::Branch(_) => "".to_string(), + Substituent::Branch(it) => it.to_string(), Substituent::Group(it) => it.to_string(), } ) } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct Branch { pub chain: Vec, pub groups: Vec>, + pub parent_alpha: Option, } impl Branch { @@ -389,28 +372,59 @@ impl Branch { Branch { chain, groups: Vec::with_capacity(len), + parent_alpha: None, + } + } + + pub fn reversed(&self) -> Branch { + let (mut chain, mut groups, parent_alpha) = { + let clone = self.clone(); + (clone.chain, clone.groups, clone.parent_alpha) + }; + chain.reverse(); + + groups = Branch::shift_chain_groups(groups); + groups.reverse(); + + Branch { + chain, + groups, + parent_alpha, } } + + fn shift_chain_groups(mut groups: Vec>) -> Vec> { + for outer in (1..groups.len()).rev() { + let link = &mut groups[outer - 1]; + let mut cache = vec![]; + + for index in (0..link.len()).rev() { + if link[index].is_chain_group() { + let element = link.remove(index); + cache.push(element); + } + } + + groups[outer].append(&mut cache); + } + groups + } +} + +impl PartialEq for Branch { + fn eq(&self, other: &Self) -> bool { + self.chain == other.chain && self.groups == other.groups + } } impl Display for Branch { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { - let out = self - .groups - .iter() - .enumerate() - .map(|(index, it)| { - format!( - "{index}: {}", - it.iter() - .map(|it| it.to_string()) - .collect::>() - .join(", ") - ) - }) - .collect::>() - .join("; "); - write!(f, "{out}") + let mut name = match process_name(self.clone()) { + Ok(it) => it, + Err(_) => panic!("unable to process {:?}", self), + }; + name.truncate(name.len() - 3); + write!(f, "{name}yl") } } @@ -423,7 +437,12 @@ impl FromStr for Branch { .map(|it| { it.trim_start_matches(|c: char| c.is_ascii_digit() || c == ':' || c == ' ') .split(", ") - .map(|str| Substituent::Group(Group::from_str(str).unwrap())) + .map(|str| { + Substituent::Group(match Group::from_str(str) { + Ok(it) => it, + Err(_) => panic!("\"{str}\" is not a valid group"), + }) + }) .collect::>() }) .collect::>>(); @@ -432,6 +451,7 @@ impl FromStr for Branch { let out = Branch { chain: vec![Atom::default(); len], groups, + parent_alpha: None, }; Ok(out) } @@ -460,7 +480,7 @@ mod tests { use super::*; use crate::graph_with; use crate::groups::group_node_tree; - use crate::molecule::Group::{Bromo, Carbonyl, Hydroxyl}; + use crate::molecule::Group::{Alkene, Bromo, Carbonyl, Hydroxyl}; use crate::spatial::GridState; use crate::test_utils::GW::{A, B}; @@ -509,6 +529,24 @@ mod tests { assert_eq!(b.to_string(), "bromo".to_string()) } + #[test] + fn branch_reversed_correctly_reverses_chain() { + let branch = Branch::from_str("0: hydroxyl, bromo, alkene; 1: chloro").unwrap(); + let reversed = branch.reversed(); + let expected = Branch::from_str("0: chloro, alkene; 1: hydroxyl, bromo").unwrap(); + + assert_eq!(reversed, expected) + } + + #[test] + fn branch_shift_chain_groups() { + let mut groups = vec![vec![Substituent::Group(Alkene)], vec![]]; + groups = Branch::shift_chain_groups(groups); + let expected = vec![vec![], vec![Substituent::Group(Alkene)]]; + + assert_eq!(groups, expected); + } + #[test] fn branch_to_string_groups_only() { let branch = Branch { @@ -517,9 +555,11 @@ mod tests { vec![Substituent::Group(Hydroxyl), Substituent::Group(Carbonyl)], vec![Substituent::Group(Bromo)], ], + parent_alpha: None, }; + let expected = Branch::from_str("0: hydroxyl, oxo; 1: bromo").unwrap(); - assert_eq!(branch.to_string(), "0: hydroxyl, oxo; 1: bromo") + assert_eq!(branch, expected); } #[test] diff --git a/src/naming.rs b/src/naming.rs index 10c5109..115851a 100644 --- a/src/naming.rs +++ b/src/naming.rs @@ -43,42 +43,43 @@ pub fn name_molecule(graph: &GridState) -> Fallible { let chain = chain::longest_chain(all_chains)?; // Group-linked branch - let branch = groups::link_groups(graph, chain)?; - // group_indexed_chain.check_chain_index() + let mut branch = groups::link_groups(graph, chain, None)?; + branch = branch.index_corrected(); - process_name(&branch).map_err(|e| Other(e.to_string())) + process_name(branch).map_err(|e| Other(e.to_string())) } -fn process_name(branch: &Branch) -> Result { +pub(crate) fn process_name(branch: Branch) -> Result { + let len = branch.chain.len() as i32; let collection = GroupCollection::new(branch); Ok(format!( "{}{}{}{}", prefix(collection.secondary_group_fragments())?, - major_numeric(branch.chain.len() as i32)?, + major_numeric(len)?, bonding(collection.chain_group_fragments())?, suffix(collection.primary_group_fragment())?, )) } -fn prefix(mut fragments: Vec) -> Result { - fragments.sort_by_key(|fragment| fragment.group.to_string()); +fn prefix(mut fragments: Vec) -> Result { + fragments.sort_by_key(|fragment| fragment.subst.to_string()); let out = fragments .into_iter() - .map(|fragment| format!("{}{}", locants(fragment.locants).unwrap(), fragment.group)) + .map(|fragment| format!("{}{}", locants(fragment.locants).unwrap(), fragment.subst)) .collect::>() .join("-"); Ok(out) } -fn bonding(mut fragments: Vec) -> Result { - fragments.sort_by_key(|fragment| fragment.group.to_string()); +fn bonding(mut fragments: Vec) -> Result { + fragments.sort_by_key(|fragment| fragment.subst.to_string()); if fragments.is_empty() { Ok("an".to_string()) } else { let mut out = fragments .into_iter() - .map(|fragment| format!("{}{}", locants(fragment.locants).unwrap(), fragment.group)) + .map(|fragment| format!("{}{}", locants(fragment.locants).unwrap(), fragment.subst)) .collect::>() .join("-"); out.insert(0, '-'); @@ -86,16 +87,25 @@ fn bonding(mut fragments: Vec) -> Result { } } -fn suffix(fragment: GroupFragment) -> Result { - let locations = locants(fragment.locants)?; - let suffix = match fragment.group { - Group::Carboxyl => return Ok("oic acid".to_string()), - Group::Carbonyl => "one", - Group::Hydroxyl => "ol", - _ => return Ok("e".to_string()), - }; +fn suffix(fragment: SubFragment) -> Result { + if let Substituent::Group(group) = fragment.subst { + let locations = locants(fragment.locants.clone())?; + let suffix = match group { + Group::Carboxyl if fragment.locants.len() == 2 => return Ok("edioic acid".to_string()), + Group::Carboxyl => return Ok("oic acid".to_string()), + Group::Carbonyl if fragment.locants == vec![0, fragment.locants.len() as i32 - 1] => { + return Ok("edial".to_string()) + } + Group::Carbonyl if fragment.locants == vec![0] => return Ok("al".to_string()), + Group::Carbonyl => "one", + Group::Hydroxyl => "ol", + _ => return Ok("e".to_string()), + }; - Ok(format!("-{locations}{suffix}")) + Ok(format!("-{locations}{suffix}")) + } else { + panic!("branch provided to suffix function") + } } /// Returns a [`String`] containing a locant prefix with a minor numeric prefix appended. @@ -115,13 +125,13 @@ fn minor_numeric(value: i32) -> Result<&'static str, NamingError> { 1 => "", 2 => "di", 3 => "tri", - 4 => "tetra", - 5 => "penta", - 6 => "hexa", - 7 => "hepta", - 8 => "octa", - 9 => "nona", - 10 => "deca", + 4 => "tetr", + 5 => "pent", + 6 => "hex", + 7 => "hept", + 8 => "oct", + 9 => "non", + 10 => "dec", _ => return Err(NamingError::GroupOccurrence(None, value)), }; Ok(out) @@ -158,7 +168,9 @@ fn major_numeric(value: i32) -> Result<&'static str, NamingError> { 27 => "heptacos", 28 => "octacos", 29 => "nonacos", - 30 => "triaconta", + 30 => "triacont", + 31 => "untriacont", + 32 => "duotriacont", _ => return Err(NamingError::CarbonCount(value)), }; Ok(out) @@ -166,30 +178,32 @@ fn major_numeric(value: i32) -> Result<&'static str, NamingError> { #[derive(Clone, Debug, PartialEq)] pub struct GroupCollection { - pub collection: Vec, + pub collection: Vec, } impl GroupCollection { - pub fn new(branch: &Branch) -> GroupCollection { + pub fn new(branch: Branch) -> GroupCollection { let mut out = GroupCollection { collection: vec![] }; - branch.groups.iter().enumerate().for_each(|(index, link)| { - link.iter().for_each(|it| { - if let Substituent::Group(group) = it { - out.push_group(group.to_owned(), index as i32); - } - }) - }); + branch + .groups + .into_iter() + .enumerate() + .for_each(|(index, link)| { + link.into_iter().for_each(|it| { + out.push_fragment(it, index as i32); + }) + }); out } - fn push_group(&mut self, group: Group, index: i32) { - let item = self.collection.iter_mut().find(|it| it.group == group); + fn push_fragment(&mut self, subst: Substituent, index: i32) { + let item = self.collection.iter_mut().find(|it| it.subst == subst); match item { Some(it) => it.locants.push(index), - None => self.collection.push(GroupFragment::new(vec![index], group)), + None => self.collection.push(SubFragment::new(vec![index], subst)), } } @@ -197,75 +211,75 @@ impl GroupCollection { let primary = self .collection .iter() - .map(|fragment| fragment.group) - .filter(|&group| group.priority().is_some()) - .max_by_key(|&group| group.priority()); + .map(|fragment| fragment.subst.to_owned()) + .filter(|group| group.priority().is_some()) + .max_by_key(|group| group.priority()); match primary { - None => Alkane, - Some(it) => it, + Some(Substituent::Group(it)) => it, + _ => Alkane, } } - pub fn primary_group_fragment(&self) -> GroupFragment { + pub fn primary_group_fragment(&self) -> SubFragment { self.collection .iter() - .find(|&fragment| fragment.group == self.primary_group()) - .map_or_else(GroupFragment::default, GroupFragment::to_owned) + .find(|&fragment| fragment.subst == Substituent::Group(self.primary_group())) + .map_or_else(SubFragment::default, SubFragment::to_owned) } - pub fn secondary_group_fragments(&self) -> Vec { + pub fn secondary_group_fragments(&self) -> Vec { self.collection .iter() - .filter(|&fragment| fragment.group != self.primary_group()) - .filter(|&fragment| !fragment.group.is_chain_group()) - .map(GroupFragment::to_owned) - .collect::>() + .filter(|&fragment| fragment.subst != Substituent::Group(self.primary_group())) + .filter(|&fragment| !fragment.subst.is_chain_group()) + .map(SubFragment::to_owned) + .collect::>() } - pub fn chain_group_fragments(&self) -> Vec { + pub fn chain_group_fragments(&self) -> Vec { self.collection .iter() - .filter(|&fragment| fragment.group.is_chain_group()) - .map(GroupFragment::to_owned) - .collect::>() + .filter(|&fragment| fragment.subst.is_chain_group()) + .map(SubFragment::to_owned) + .collect::>() } } #[derive(Clone, Debug, PartialEq)] -pub struct GroupFragment { - locants: Vec, - group: Group, +pub struct SubFragment { + pub locants: Vec, + pub subst: Substituent, } -impl GroupFragment { - pub fn new(locants: Vec, group: Group) -> GroupFragment { - GroupFragment { locants, group } +impl SubFragment { + pub fn new(locants: Vec, subst: Substituent) -> SubFragment { + SubFragment { locants, subst } } } -impl Default for GroupFragment { +impl Default for SubFragment { fn default() -> Self { - GroupFragment { + SubFragment { locants: vec![0], - group: Alkane, + subst: Substituent::Group(Alkane), } } } -impl Display for GroupFragment { +impl Display for SubFragment { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, "{}{}", locants(self.locants.clone()).unwrap(), - self.group + self.subst ) } } #[derive(Debug, Error, PartialEq)] -enum NamingError { +pub enum NamingError { #[error("A branch was found with too many carbons ({}).", .0)] CarbonCount(i32), #[error("Found too many occurrences of the {:?} group ({}).", .0, .1)] @@ -282,7 +296,7 @@ mod tests { fn prefix_creates_correct_strings() { let branch = Branch::from_str("0: bromo, iodo; 1: oxo, hydroxyl; 2: bromo, hydroxyl").unwrap(); - let collection = GroupCollection::new(&branch); + let collection = GroupCollection::new(branch); let str = prefix(collection.secondary_group_fragments()).unwrap(); assert_eq!(str, "1,3-dibromo-2,3-dihydroxyl-1-iodo") @@ -292,12 +306,12 @@ mod tests { fn collect_groups_aggregates_correctly() { let branch = Branch::from_str("0: bromo, iodo; 1: oxo, hydroxyl; 2: bromo, hydroxyl").unwrap(); - let groups = GroupCollection::new(&branch); + let groups = GroupCollection::new(branch); let expected = vec![ - GroupFragment::new(vec![0, 2], Bromo), - GroupFragment::new(vec![0], Iodo), - GroupFragment::new(vec![1], Carbonyl), - GroupFragment::new(vec![1, 2], Hydroxyl), + SubFragment::new(vec![0, 2], Substituent::Group(Bromo)), + SubFragment::new(vec![0], Substituent::Group(Iodo)), + SubFragment::new(vec![1], Substituent::Group(Carbonyl)), + SubFragment::new(vec![1, 2], Substituent::Group(Hydroxyl)), ]; assert_eq!(groups.collection, expected); @@ -307,8 +321,8 @@ mod tests { fn primary_group_doesnt_ignore_halogens() { let collection = GroupCollection { collection: vec![ - GroupFragment::new(vec![0], Bromo), - GroupFragment::new(vec![0], Chloro), + SubFragment::new(vec![0], Substituent::Group(Bromo)), + SubFragment::new(vec![0], Substituent::Group(Chloro)), ], }; assert_eq!(collection.primary_group(), Alkane); diff --git a/src/spatial.rs b/src/spatial.rs index e1c701b..20a3e2f 100644 --- a/src/spatial.rs +++ b/src/spatial.rs @@ -35,7 +35,7 @@ impl GridState { GridState { cells, size: Vec2::xy(width, height), - cursor: Vec2::xy((width - 1) / 2, (height - 1) / 2), + cursor: Vec2::xy(width / 2, height / 2), } } diff --git a/src/validation.rs b/src/validation.rs index 03d6d9a..0599406 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -7,6 +7,7 @@ use crate::chain; use crate::groups::InvalidGraphError::Discontinuity; use crate::groups::{Fallible, InvalidGraphError}; use crate::molecule::{Atom, Branch, Cell}; +use crate::naming::{GroupCollection, SubFragment}; use crate::pointer::Pointer; use crate::spatial::GridState; use ruscii::spatial::Vec2; @@ -68,9 +69,45 @@ pub fn check_valence(atoms: Vec<&Atom>, graph: &GridState) -> Fallible<()> { Ok(()) } -/// Checks if chain indexes are in the correct direction. -pub fn chain_in_correct_direction(branch: &Branch) -> bool { - true +impl Branch { + /// Checks if chain indexes are in the correct direction. + pub fn index_corrected(self) -> Branch { + let original = self.clone(); + let reversed = self.reversed(); + let original_collection = GroupCollection::new(original.clone()); + let reversed_collection = GroupCollection::new(reversed.clone()); + + match cmp( + original_collection.primary_group_fragment(), + reversed_collection.primary_group_fragment(), + ) { + Ordering::Less => return original, + Ordering::Greater => return reversed, + Ordering::Equal => {} + } + + // TODO add more cases: + // lowest-numbered locants for multiple bonds + // lowest-numbered locants for prefixes + + original + } +} + +fn cmp(first: SubFragment, second: SubFragment) -> Ordering { + let mut first_locants = first.locants; + first_locants.sort(); + let mut second_locants = second.locants; + second_locants.sort(); + + for (index, locant) in first_locants.iter().enumerate() { + match locant.cmp(&second_locants[index]) { + Ordering::Equal => continue, + Ordering::Less => return Ordering::Less, + Ordering::Greater => return Ordering::Greater, + } + } + Ordering::Equal } #[cfg(test)]