Java tutorial
/* * This file is part of the SIRIUS library for analyzing MS and MS/MS data * * Copyright (C) 2013-2015 Kai Dhrkop * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU General Public License along with SIRIUS. If not, see <http://www.gnu.org/licenses/>. */ package de.unijena.bioinf.ChemistryBase.chem; import com.google.common.collect.Range; import de.unijena.bioinf.ChemistryBase.chem.utils.*; import de.unijena.bioinf.ChemistryBase.ms.Deviation; import java.io.IOException; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Give access to all chemical elements and ions. This class should be seen as singleton, although it's * possible to create multiple PeriodicTables * All this information are parsed from a json file in the ChemistryBase library. * <p/> * The PeriodicTable is not thread-safe, because in practice there should be only read-accesses. For write access, * you have to do the synchronisation yourself. * <p/> * <pre> * PeriodicTable.getInstance().getByName("C").getMass(); * PeriodicTable.getInstance().parse("C6H12O6", myAtomVisitor) * </pre> */ public class PeriodicTable implements Iterable<Element>, Cloneable { /* STATIC */ private static PeriodicTable instance; private static ThreadLocal<PeriodicTable> localInstance = new ThreadLocal<PeriodicTable>(); private static final ArrayList<PeriodicTable> instanceStack = new ArrayList<PeriodicTable>(); private static final ThreadLocal<ArrayList<PeriodicTable>> localInstanceStack = new ThreadLocal<ArrayList<PeriodicTable>>(); private static int threadLocal; private final static Pattern IONTYPE_PATTERN = Pattern.compile("[\\[\\]()+-]"); private final static Pattern IONTYPE_NUM_PATTERN = Pattern.compile("^\\d+$"); /** * @return current enabled periodic table instance */ public static PeriodicTable getInstance() { if (!isThreadLocal()) return instance; return getLocalInstance(); } private static PeriodicTable getLocalInstance() { final PeriodicTable pt = localInstance.get(); if (pt == null) return instance; return pt; } private static PeriodicTable push(PeriodicTable pt) { final ArrayList<PeriodicTable> stack; final PeriodicTable before; final boolean threadLocal = isThreadLocal(); if (threadLocal) { final ArrayList<PeriodicTable> st = localInstanceStack.get(); if (st == null) { stack = new ArrayList<PeriodicTable>(); localInstanceStack.set(stack); } else stack = st; final PeriodicTable t = localInstance.get(); if (t == null) before = instance; else before = t; } else { stack = instanceStack; before = instance; } stack.add(before); if (threadLocal) localInstance.set(pt); else instance = pt; return pt; } /** * Add a new empty periodic table to the stack and enable it. Can be used to change temporarily a periodic table * * @return the added periodic table */ public static PeriodicTable push() { return push(new PeriodicTable()); } /** * add a copy of the current periodic table to the stack. Can be used to change temporarily a periodic table * * @return the added periodic table */ public static PeriodicTable pushCopy() { return push(instance.clone()); } /** * removes the last periodic table which was added to the stack and enable the previous table. Use together with * push or pushCopy to change temporarily a periodic table. * * @return */ public static PeriodicTable pop() { final boolean threadLocal = isThreadLocal(); final PeriodicTable ret; if (threadLocal) { ret = localInstance.get(); final ArrayList<PeriodicTable> stack = localInstanceStack.get(); if (stack.size() == 0) localInstance.set(null); else { localInstance.set(stack.get(stack.size() - 1)); stack.remove(stack.size() - 1); } } else { ret = instance; if (instanceStack.size() == 0) throw new RuntimeException("No further periodic table in stack"); instance = instanceStack.get(instanceStack.size() - 1); instanceStack.remove(instanceStack.size() - 1); } return ret; } /** * returns true if the periodic table may be different in different threads. * * @return */ public static boolean isThreadLocal() { return threadLocal > 0; } /** * Enable thread local periodic tables. If set to true, each change of a periodic table (either by push or by * direct modification) affect only the current thread. Be careful with this option, because it may reduce * the performance. Especially, it may be dangerous to set this option back to false, after enable it, because * the standard access to the periodic table is not synchronized * * @param value */ public static synchronized void setThreadLocal(boolean value) { synchronized (IsotopicDistribution.class) { threadLocal += (value ? 1 : -1); } if (!value) localInstance.set(null); } static { instance = new PeriodicTable(); try { new PeriodicTableBlueObeliskReader().readFromClasspath(instance); new PeriodicTableJSONReader().readFromClasspath(instance, "/additional_elements.json"); //new PeriodicTableJSONReader().readFromClasspath(instance); instance.cache.addDefaultAlphabet(); instance.setDistribution(new IsotopicDistributionBlueObeliskReader().getFromClasspath()); instance.addDefaultIons(); } catch (IOException e) { e.printStackTrace(); } } public PrecursorIonType getPrecursorIonTypeForEI() { return EI_TYPE; } private IonMode[] POSITIVE_ION_MODES, NEGATIVE_ION_MODES; private Charge POSITIVE_IONIZATION, NEGATIVE_IONIZATION; private IonMode PROTONATION, DEPROTONATION, INTRINSICALLY_CHARGED_POSITIVE, INTRINSICALLY_CHARGED_NEGATIVE; private Ionization ELECTRON_IONIZATION; private PrecursorIonType EI_TYPE, UNKNOWN_POSITIVE_IONTYPE, UNKNOWN_NEGATIVE_IONTYPE; public Iterable<Ionization> getKnownIonModes(int charge) { if (Math.abs(charge) != 1) throw new IllegalArgumentException("Do not support multiple charges yet"); if (charge > 0) { return Arrays.asList((Ionization[]) POSITIVE_ION_MODES); } else { return Arrays.asList((Ionization[]) NEGATIVE_ION_MODES); } } /** * returns a list of PrecursorIonType instances with the given charge * ion types at the beginning of the list are more common/likely than ion types * at the end of the list * @param charge * @return */ public Iterable<PrecursorIonType> getKnownLikelyPrecursorIonizations(int charge) { if (Math.abs(charge) != 1) throw new IllegalArgumentException("Do not support multiple charges yet"); final HashSet<PrecursorIonType> ions = new HashSet<PrecursorIonType>(knownIonTypes.values()); final ArrayList<PrecursorIonType> likely = new ArrayList<PrecursorIonType>(); if (charge > 0) { likely.add(ionByName("[M+H]+")); likely.add(ionByName("[M]+")); likely.add(ionByName("[M+H-H2O]+")); likely.add(ionByName("[M+Na]+")); for (PrecursorIonType i : likely) ions.remove(i); likely.addAll(ions); } else { likely.add(ionByName("[M-H]-")); likely.add(ionByName("[M]-")); for (PrecursorIonType i : likely) ions.remove(i); likely.addAll(ions); } return likely; } private void addDefaultIons() { // ION MODES PROTONATION = new IonMode(1, "[M+H]+", MolecularFormula.parse("H")); DEPROTONATION = new IonMode(-1, "[M-H]-", MolecularFormula.parse("H").negate()); this.UNKNOWN_NEGATIVE_IONTYPE = new PrecursorIonType(new Charge(-1), MolecularFormula.emptyFormula(), MolecularFormula.emptyFormula()); this.UNKNOWN_POSITIVE_IONTYPE = new PrecursorIonType(new Charge(1), MolecularFormula.emptyFormula(), MolecularFormula.emptyFormula()); this.POSITIVE_ION_MODES = new IonMode[] { new IonMode(1, "[M+K]+", MolecularFormula.parse("K")), new IonMode(1, "[M+Na]+", MolecularFormula.parse("Na")), PROTONATION }; this.NEGATIVE_ION_MODES = new IonMode[] { new IonMode(-1, "[M+Cl]-", MolecularFormula.parse("Cl")), DEPROTONATION }; this.POSITIVE_IONIZATION = new Charge(1); this.NEGATIVE_IONIZATION = new Charge(-1); this.INTRINSICALLY_CHARGED_NEGATIVE = new IonMode(-1, "[M]-", MolecularFormula.emptyFormula()); this.INTRINSICALLY_CHARGED_POSITIVE = new IonMode(1, "[M]+", MolecularFormula.emptyFormula()); this.ELECTRON_IONIZATION = new ElectronIonization(); this.EI_TYPE = new PrecursorIonType(ELECTRON_IONIZATION, MolecularFormula.emptyFormula(), MolecularFormula.emptyFormula()); // ADDUCTS final String[] adductsPositive = new String[] { "[M+H]+", "[M]+", "[M+K]+", "[M+K-2H]+", "[M+OH]+", "[M+Na]+", "[M+H-H2O]+", "[M-H+Na]+", "[M+Na2-H]+", "[M+Na2-H]+", "[M+NH3+H]+", "[(M+NH3)+H]+", "[M+NH4]+", "[M+H-C6H10O4]+", "[M+H-C6H10O5]+" }; final String[] adductsNegative = new String[] { "[M-H]-", "[M]-", "[M-2H]-", "[M+K-2H]-", "[M-OH]-", "[M+Cl]-", "[M-H+OH]-", "[M+HCOO-]-", "[M+CH3COOH-H]-", "[(M+CH3COOH)-H]-" }; final HashMap<String, PrecursorIonType> positiveIonTypes = new HashMap<String, PrecursorIonType>(); for (String pos : adductsPositive) { positiveIonTypes.put(pos, parseIonType(pos)); assert positiveIonTypes.get(pos).getIonization().getCharge() > 0; } final HashMap<String, PrecursorIonType> negativeIonTypes = new HashMap<String, PrecursorIonType>(); for (String neg : adductsNegative) { negativeIonTypes.put(neg, parseIonType(neg)); assert negativeIonTypes.get(neg).getIonization().getCharge() < 0; } knownIonTypes.putAll(positiveIonTypes); knownIonTypes.putAll(negativeIonTypes); //knownIonTypes.put("[M+?]+", UNKNOWN_POSITIVE_IONTYPE); //knownIonTypes.put("[M+?]-", UNKNOWN_NEGATIVE_IONTYPE); } private PrecursorIonType parseIonType(String name) { // tokenize String final ArrayList<String> tokens = new ArrayList<String>(); final Matcher m = IONTYPE_PATTERN.matcher(name); int lastPos = 0; while (m.find()) { if (m.start() > lastPos) tokens.add(name.substring(lastPos, m.start())); tokens.add(m.group()); lastPos = m.end(); } if (lastPos < name.length()) tokens.add(name.substring(lastPos, name.length())); int state = 0; final ArrayList<MolecularFormula> adducts = new ArrayList<MolecularFormula>(); final ArrayList<MolecularFormula> insourceFrags = new ArrayList<MolecularFormula>(); boolean isAdd = true; int number = 1; for (int k = 0; k < tokens.size(); ++k) { final String token = tokens.get(k).trim(); final char c = token.charAt(0); switch (c) { case '(': if (number != 1) { throw new IllegalArgumentException( "Do not support multiplier before a molecular formula: '" + name + "'"); } else break; case ')': break; case '[': break; // ignore case ']': break; // ignore case '+': if (number != 1) { throw new IllegalArgumentException("Do not support multiple charges: '" + name + "'"); } else { isAdd = true; break; } case '-': if (number != 1) { throw new IllegalArgumentException("Do not support multiple charges: '" + name + "'"); } else { isAdd = false; break; } case 'M': if (number != 1) { throw new IllegalArgumentException("Do not support multimeres: '" + name + "'"); } else if (!isAdd) { throw new IllegalArgumentException("Invalid format of ion type: '" + name + "'"); } else break; default: { if (IONTYPE_NUM_PATTERN.matcher(token).find()) { // is a number number = Integer.parseInt(token); } else { // should be a molecular formula MolecularFormula f = MolecularFormula.parse(token); if (number != 1) { f = f.multiply(number); } if (isAdd) { adducts.add(f); } else { insourceFrags.add(f); } isAdd = true; number = 1; } } } } final int charge = (isAdd ? 1 : -1); // find ionization mode Ionization usedIonMode = null; final IonMode[] ionModes = (charge > 0) ? POSITIVE_ION_MODES : NEGATIVE_ION_MODES; for (IonMode ion : ionModes) { if (ion.getAtoms().atomCount() > 0) { // search for adduct containing this ion int found = -1; for (int i = 0; i < adducts.size(); ++i) { if (ion.getAtoms().equals(adducts.get(i))) { found = i; break; } } if (found >= 0) { adducts.remove(found); usedIonMode = ion; } else { for (int i = 0; i < adducts.size(); ++i) { if (adducts.get(i).isSubtractable(ion.getAtoms())) { found = i; break; } } if (found >= 0) { usedIonMode = ion; adducts.set(found, adducts.get(found).subtract(ion.getAtoms())); } } } else if (ion.getAtoms().atomCount() < 0) { // search for loss containing this ion int found = -1; MolecularFormula neg = ion.getAtoms().negate(); for (int i = 0; i < insourceFrags.size(); ++i) { if (neg.equals(insourceFrags.get(i))) { found = i; break; } } if (found >= 0) { insourceFrags.remove(found); usedIonMode = ion; } else { for (int i = 0; i < insourceFrags.size(); ++i) { if (insourceFrags.get(i).isSubtractable(neg)) { found = i; break; } } if (found >= 0) { usedIonMode = ion; insourceFrags.set(found, insourceFrags.get(found).subtract(neg)); } } } if (usedIonMode != null) break; } if (usedIonMode == null) { usedIonMode = charge > 0 ? INTRINSICALLY_CHARGED_POSITIVE : INTRINSICALLY_CHARGED_NEGATIVE; } MolecularFormula adduct = MolecularFormula.emptyFormula(); for (MolecularFormula f : adducts) adduct = adduct.add(f); MolecularFormula insource = MolecularFormula.emptyFormula(); for (MolecularFormula f : insourceFrags) insource = insource.add(f); return new PrecursorIonType(usedIonMode, insource, adduct); } public IonMode getProtonation() { return PROTONATION; } public IonMode getDeprotonation() { return DEPROTONATION; } public PrecursorIonType getPrecursorIonTypeFromIonization(Ionization ion) { if (ion instanceof Charge) { if (ion.getCharge() == 1) return UNKNOWN_POSITIVE_IONTYPE; else if (ion.getCharge() == -1) return UNKNOWN_NEGATIVE_IONTYPE; else throw new IllegalArgumentException("Multiple charges are not supported yet"); } for (PrecursorIonType i : knownIonTypes.values()) { if (i.getIonization().equals(ion) && i.getAdduct().atomCount() == 0 && i.getInSourceFragmentation().atomCount() == 0) return i; } return new PrecursorIonType(ion, MolecularFormula.emptyFormula(), MolecularFormula.emptyFormula()); } public PrecursorIonType getUnknownPrecursorIonType(int charge) { if (charge != -1 && charge != 1) throw new IllegalArgumentException("Multiple charges are not allowed!"); if (charge > 0) return UNKNOWN_POSITIVE_IONTYPE; else return UNKNOWN_NEGATIVE_IONTYPE; } private final static class ElementStack { private Element element; private short amount; private ElementStack neighbour; private void set(Element element, int amount) { this.element = element; if (amount > Short.MAX_VALUE || amount < Short.MIN_VALUE) { throw new RuntimeException("Element number exceeds formula space: " + amount); } this.amount = (short) amount; } } /** * Regular Expression for molecular formulas regarding all chemical elements in PeriodicTable */ private Pattern pattern; private final HashMap<String, Element> nameMap; private final ArrayList<Element> elements; private IsotopicDistribution distribution; private MolecularFormula emptyFormula; private final HashMap<String, PrecursorIonType> knownIonTypes; /** * Cache which is used to build molecular formulas with shared structure */ final TableSelectionCache cache; PeriodicTable() { this.elements = new ArrayList<Element>(); this.nameMap = new HashMap<String, Element>(); this.cache = new TableSelectionCache(this, TableSelectionCache.DEFAULT_MAX_COMPOMERE_SIZE); this.knownIonTypes = new HashMap<String, PrecursorIonType>(); this.distribution = new IsotopicDistribution(this); this.emptyFormula = null; } PeriodicTable(PeriodicTable pt) { this.elements = new ArrayList<Element>(pt.elements); this.nameMap = new HashMap<String, Element>(pt.nameMap); this.pattern = pt.pattern; this.knownIonTypes = new HashMap<String, PrecursorIonType>(); // new cache =( this.cache = new TableSelectionCache(this, TableSelectionCache.DEFAULT_MAX_COMPOMERE_SIZE); this.emptyFormula = null; this.distribution = new IsotopicDistribution(this); distribution.merge(pt.distribution); } /** * @return the empty formula. This object is cached as molecular formulas are immutable */ public MolecularFormula emptyFormula() { if (emptyFormula == null) emptyFormula = MolecularFormula.fromCompomer(cache.getSelectionFor(new BitSet()), new short[0]); return emptyFormula; } public void addElement(String name, String symbol, double mass, int valence) { if (nameMap.containsKey(symbol)) throw new IllegalArgumentException("There is already an element with name '" + symbol + "'"); elements.add(new Element(elements.size(), name, symbol, mass, valence)); nameMap.put(symbol, elements.get(elements.size() - 1)); pattern = null; } /** * Adds a new ion type to the list of known/common ion types * * @param name name of the ion type * @param ionType ion type that should be added * @return true if ion type is added, false if the ion type was already in the set * @throws IllegalArgumentException if the name is already used for a different ion type */ public boolean addCommonIonType(String name, PrecursorIonType ionType) { if (knownIonTypes.containsKey(name)) { if (ionType.equals(knownIonTypes.get(ionType))) return false; else throw new IllegalArgumentException("There is already an ionization with name '" + name + "'"); } knownIonTypes.put(name, ionType); return true; } /** * @see PeriodicTable#addCommonIonType(String, PrecursorIonType) * uses the normalized name of the ion type */ public boolean addCommonIonType(PrecursorIonType ionType) { return addCommonIonType(ionType.toString(), ionType); } /** * @return the regular expression pattern that is used to parse molecular formulas */ public Pattern getPattern() { if (pattern == null) refreshRegularExpression(); return pattern; } @Override protected PeriodicTable clone() { return new PeriodicTable(this); } public IsotopicDistribution getDistribution() { return distribution; } public void setDistribution(IsotopicDistribution distribution) { this.distribution = distribution; for (Element e : elements) { final Isotopes d = distribution.getIsotopesFor(e); if (d == null) continue; double minMass = Double.MAX_VALUE; for (int i = 0; i < d.getNumberOfIsotopes(); ++i) { if (d.getAbundance(i) > 0 && d.getMass(i) < minMass) { minMass = d.getMass(i); } } e.mass = minMass; e.nominalMass = (int) (Math.round(minMass)); } } private void refreshRegularExpression() { if (elements.isEmpty()) { pattern = Pattern.compile(""); return; } final StringBuilder buffer = new StringBuilder(); final Element[] orderedElements = elements.toArray(new Element[elements.size()]); Arrays.sort(orderedElements, new Comparator<Element>() { @Override public int compare(Element o1, Element o2) { return o2.getSymbol().length() - o1.getSymbol().length(); } }); final Iterator<Element> elementIterator = Arrays.asList(orderedElements).iterator(); buffer.append("(\\)|"); buffer.append(elementIterator.next().getSymbol()); while (elementIterator.hasNext()) { buffer.append("|").append(elementIterator.next()); } buffer.append(")(\\d*)|\\("); this.pattern = Pattern.compile(buffer.toString()); } /** * build a bitset of an array of elements. The i-th bit is set if the i-th element is contained * in the array */ BitSet bitsetOfElements(Element... elements) { final int maxId = Collections.max(Arrays.asList(elements), new Comparator<Element>() { @Override public int compare(Element o1, Element o2) { return o1.getId() - o2.getId(); } }).getId(); final BitSet bitset = new BitSet(maxId + 1); for (Element e : elements) { bitset.set(e.getId()); } return bitset; } /** * @return an immutable list of ions */ public Collection<PrecursorIonType> getIons() { return knownIonTypes.values(); } /** * return the element with the given Id * * @param id * @return an Element or null if there is no element with this id */ public Element get(int id) { return elements.get(id); } /** * returns an array with elements corresponding to the given names. The i-th element in the * array has the i-th name of the given names-array. If a name appears multiple times in the * names array, so it does in the returned array. If a name is not found, the position in the * array is filled with null. * * @param names * @return array with Element instances or nulls. */ public Element[] getAllByName(String... names) { final Element[] elements = new Element[names.length]; for (int i = 0; i < names.length; ++i) { elements[i] = getByName(names[i]); } return elements; } /** * return the element with the given symbol. * * @param name * @return an Element or null if there is no element with this symbol */ public Element getByName(String name) { return nameMap.get(name); } /** * Search for a TableSelection which contains at least all the given elements. Use this, if you * want to control onto which TableSelection your molecules are built. * * @param elements * @return */ public TableSelection getSelectionFor(Element... elements) { return getSelectionFor(bitsetOfElements(elements)); } /** * Search for a TableSelection which contains at least all the given elements. Use this, if you * want to control onto which TableSelection your molecules are built. * * @return */ public TableSelection getSelectionFor(BitSet bitset) { return cache.getSelectionFor(bitset); } public PrecursorIonType ionByMass(double mass, double absError) { return ionByMass(mass, absError, 0); } /** * search for a known ion which mass is corresponding to the given mass while considering the * given mass error. If there multiple ions in the mass window, the method returns the ion with * the lowest mass error. * * @param mass * @param absError * @return an ion with the given mass or null if no ion is found */ public PrecursorIonType ionByMass(double mass, double absError, int charge) { PrecursorIonType minIon = null; double minDistance = Double.MAX_VALUE; for (PrecursorIonType iontype : knownIonTypes.values()) { final Ionization ion = iontype.getIonization(); if (charge != 0 && ion.getCharge() != charge) continue; final double abw = Math.abs(iontype.getModificationMass() - mass); if (abw < minDistance) { minDistance = abw; minIon = iontype; } } if (minDistance < absError) return minIon; else return null; } /** * search for an ion with the given name. Usually, the names are in the format '[M'[+-]X']'[+-] where * X is a molecular formula, for example [M+H2O]+. * <p/> * [M+H]+ */ public PrecursorIonType ionByName(String name) { if (knownIonTypes.containsKey(name)) return knownIonTypes.get(name); else return parseIonType(name); } /** * Calculate for a given alphabet the maximal and minimal mass defects of isotopes. * * @param alphabet chemical alphabet * @param deviation allowed mass deviation * @param monomz m/z of monoisotopic peak * @param peakOffset integer distance between isotope peak and monoisotopic peak (minimum: 1) * @return an interval which should contain the isotopic peak */ public Range<Double> getIsotopicMassWindow(ChemicalAlphabet alphabet, Deviation deviation, double monomz, int peakOffset) { if (peakOffset < 1) throw new IllegalArgumentException("Expect a peak offset of at least 1"); final IsotopicDistribution dist = getDistribution(); double minmz = Double.POSITIVE_INFINITY; double maxmz = Double.NEGATIVE_INFINITY; for (Element e : alphabet) { final Isotopes iso = dist.getIsotopesFor(e); for (int k = 1; k < iso.getNumberOfIsotopes(); ++k) { final int i = iso.getIntegerMass(k) - e.getIntegerMass(); if (i > peakOffset) break; double diff = iso.getMassDifference(k) - i; diff *= (peakOffset / i); minmz = Math.min(minmz, diff); maxmz = Math.max(maxmz, diff); } } final double a = monomz + peakOffset + minmz; final double b = monomz + peakOffset + maxmz; return Range.closed(a - deviation.absoluteFor(a), b + deviation.absoluteFor(b)); } /** * returns an iterator which yields each element in the table in order of their ids. */ @Override public Iterator<Element> iterator() { return Collections.unmodifiableList(elements).iterator(); } /** * @return number of elements in the table */ public int numberOfElements() { return elements.size(); } /** * parses a formula and invoke {@link FormulaVisitor#visit(Element, int)} for each atom. * Remark that there is no guarantee that for each atom type this method is called only one time. * Use this, if you want to build your own molecular formula type without using MolecularFormula. * Another advantage of this function is the independence from TableSelection. * * @param formula * @param visitor */ public void parse(String formula, FormulaVisitor<?> visitor) { if (formula.indexOf('(') < 0) { parseUnstackedFormula(formula, visitor); } else { parseStackedFormula(formula, visitor); } } private void parseStackedFormula(String formula, FormulaVisitor<?> visitor) { final Matcher matcher = pattern.matcher(formula); final ArrayDeque<ElementStack> stack = new ArrayDeque<ElementStack>(); while (matcher.find()) { switch (matcher.group().charAt(0)) { case '(': stack.push(new ElementStack()); break; case ')': final String multiplyStr = matcher.group(2); final int multiply = (multiplyStr != null && multiplyStr.length() > 0) ? Integer.parseInt(multiplyStr) : 1; ElementStack stackItem = stack.pop(); ElementStack prev = stack.isEmpty() ? null : stack.pop(); while (stackItem.neighbour != null) { stackItem = stackItem.neighbour; if (prev == null) { visitor.visit(stackItem.element, stackItem.amount * multiply); } else { prev.set(stackItem.element, stackItem.amount * multiply); final ElementStack add = new ElementStack(); add.neighbour = prev; prev = add; } } if (prev != null) stack.push(prev); break; default: ElementStack last = stack.isEmpty() ? null : stack.pop(); final String elementName = matcher.group(1); final String elementAmount = matcher.group(2); final Element element = getByName(elementName); final int amount = elementAmount != null && elementAmount.length() > 0 ? Integer.parseInt(elementAmount) : 1; if (last == null) { visitor.visit(element, amount); } else { last.set(element, amount); final ElementStack add = new ElementStack(); add.neighbour = last; last = add; stack.push(last); } } } } private void parseUnstackedFormula(String formula, FormulaVisitor<?> visitor) { final int multiplier; if (formula.isEmpty()) return; if (Character.isDigit(formula.charAt(0))) { int lastnum = 0; while (lastnum < formula.length() && Character.isDigit(formula.charAt(lastnum))) ++lastnum; multiplier = Integer.parseInt(formula.substring(0, lastnum)); formula = formula.substring(lastnum, formula.length()); } else multiplier = 1; final Matcher matcher = getPattern().matcher(formula); while (matcher.find()) { final String elementName = matcher.group(1); final String elementAmount = matcher.group(2); final Element element = getByName(elementName); final int amount = multiplier * (elementAmount != null && elementAmount.length() > 0 ? Integer.parseInt(elementAmount) : 1); visitor.visit(element, amount); } } }