001package com.hfg.chem;
002
003import java.util.ArrayList;
004import java.util.Collections;
005import java.util.HashMap;
006import java.util.HashSet;
007import java.util.List;
008import java.util.Map;
009import java.util.Set;
010import java.util.Stack;
011
012import com.hfg.util.CharUtil;
013import com.hfg.util.CompareUtil;
014import com.hfg.util.StringUtil;
015import com.hfg.util.collection.CollectionUtil;
016
017//------------------------------------------------------------------------------
018/**
019 Elemental composition and mass tracking object for any organic or inorganic molecule.
020 <div>
021 @author J. Alex Taylor, hairyfatguy.com
022 </div>
023 */
024//------------------------------------------------------------------------------
025// com.hfg XML/HTML Coding Library
026//
027// This library is free software; you can redistribute it and/or
028// modify it under the terms of the GNU Lesser General Public
029// License as published by the Free Software Foundation; either
030// version 2.1 of the License, or (at your option) any later version.
031//
032// This library is distributed in the hope that it will be useful,
033// but WITHOUT ANY WARRANTY; without even the implied warranty of
034// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
035// Lesser General Public License for more details.
036//
037// You should have received a copy of the GNU Lesser General Public
038// License along with this library; if not, write to the Free Software
039// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
040//
041// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
042// jataylor@hairyfatguy.com
043//------------------------------------------------------------------------------
044
045public class MatterImpl implements Matter, Cloneable, Comparable<MatterImpl>
046{
047
048   //##########################################################################
049   // PRIVATE FIELDS
050   //##########################################################################
051
052   private Map<Element, Float>     mElementalComposition;
053   private Double                  mMonoisotopicMass;
054   private Double                  mAverageMass;
055
056   private boolean                 mMonoisotopicMassIsUserSet;
057   private boolean                 mAverageMassIsUserSet;
058
059   private Integer mHashCode;
060
061   private static Set<Character> sSaltIndicatorsInChemicalFormulas = new HashSet<>(4);
062
063   static
064   {
065      sSaltIndicatorsInChemicalFormulas.add('.');
066      sSaltIndicatorsInChemicalFormulas.add('·');
067      sSaltIndicatorsInChemicalFormulas.add('•');
068      sSaltIndicatorsInChemicalFormulas.add('*');
069   }
070
071   //##########################################################################
072   // CONSTRUCTORS
073   //##########################################################################
074
075   //--------------------------------------------------------------------------
076   public MatterImpl()
077   {
078   }
079
080   //--------------------------------------------------------------------------
081   public MatterImpl(Map<Element, Float> inMap)
082   {
083      setElementalComposition(inMap);
084   }
085
086   //--------------------------------------------------------------------------
087   public MatterImpl(Matter inInitialValue)
088   {
089      if (inInitialValue != null)
090      {
091         setMonoisotopicMass(inInitialValue.getMonoisotopicMass());
092         setAverageMass(inInitialValue.getAverageMass());
093         setElementalComposition(inInitialValue.getElementalComposition());
094      }
095   }
096
097   //##########################################################################
098   // PUBLIC METHODS
099   //##########################################################################
100
101
102   //--------------------------------------------------------------------------
103   // TODO: Add isotope support
104   public static Matter fromChemicalFormula(String inChemicalFormula)
105   {
106      MatterImpl matter = null;
107
108      Stack<Character> enclosingCharStack = new Stack<>();
109
110      if (StringUtil.isSet(inChemicalFormula))
111      {
112         matter = new MatterImpl();
113
114         MatterImpl.FormulaSubBlock currentSubBlock = matter.new FormulaSubBlock();
115         MatterImpl.FormulaSubBlock topBlock = currentSubBlock;
116         Stack<MatterImpl.FormulaSubBlock> blockStack = new Stack<>();
117         blockStack.push(topBlock);
118
119         String chemicaFormulaString = inChemicalFormula.trim();
120
121         for (int i = 0; i < chemicaFormulaString.length(); i++)
122         {
123            char currentChar = chemicaFormulaString.charAt(i);
124
125            if ('(' == currentChar
126                  || '[' == currentChar)
127            {
128               enclosingCharStack.push(currentChar);
129               MatterImpl.FormulaSubBlock subBlock = matter.new FormulaSubBlock().setBracketType(currentChar);
130               currentSubBlock.addSubBlock(subBlock);
131               blockStack.push(subBlock);
132
133               currentSubBlock = subBlock;
134            }
135            else if (')' == currentChar
136                     || ']' == currentChar)
137            {
138               // Ending enclosure w/o a starting enclosure?
139               if (0 == enclosingCharStack.size())
140               {
141                  throw new ChemicalFormulaParseException("The chemical formula " + StringUtil.singleQuote(chemicaFormulaString) + " has an unbalanced '" + currentChar + "' near position " + (i + 1) + "!");
142               }
143
144               char openingChar = enclosingCharStack.pop();
145               // Mismatched enclosure characters?
146               if (('(' == openingChar && ')' != currentChar)
147                   || ('[' == openingChar && ']' != currentChar))
148               {
149                  throw new ChemicalFormulaParseException("The chemical formula " + StringUtil.singleQuote(chemicaFormulaString) + " has an unbalanced '" + currentChar + "' near position " + (i + 1) + "!");
150               }
151
152               currentSubBlock.close();
153
154               i++;
155
156               String countString = "";
157               Character convertedSubscriptChar = null;
158               while (i < chemicaFormulaString.length())
159               {
160                  char countChar = chemicaFormulaString.charAt(i);
161                  if (Character.isDigit(countChar)
162                      || (convertedSubscriptChar = convertSubscriptChar(countChar)) != null)
163                  {
164                     countString += (convertedSubscriptChar != null ? convertedSubscriptChar : countChar);
165                     i++;
166                  }
167                  else
168                  {
169                     break;
170                  }
171               }
172
173               if (StringUtil.isSet(countString))
174               {
175                  currentSubBlock.setCount(Integer.parseInt(countString));
176               }
177
178               if (i < chemicaFormulaString.length())
179               {
180                  i--;
181               }
182
183               blockStack.pop();
184               currentSubBlock = blockStack.peek();
185            }
186            else
187            {
188               currentSubBlock.append(currentChar);
189            }
190         }
191
192         matter.setElementalComposition(topBlock.getChemicalComposition());
193      }
194
195      return matter;
196   }
197
198   //--------------------------------------------------------------------------
199   @Override
200   public int hashCode()
201   {
202      if (null == mHashCode)
203      {
204         int hashcode = 31;
205         if (mElementalComposition != null)
206         {
207            for (Element element : mElementalComposition.keySet())
208            {
209               hashcode = hashcode + 31 * element.hashCode() * mElementalComposition.get(element).intValue();
210            }
211         }
212
213         mHashCode = hashcode;
214      }
215
216      return mHashCode;
217   }
218
219   //--------------------------------------------------------------------------
220   @Override
221   public boolean equals(Object inObj)
222   {
223      boolean result = false;
224
225      if (inObj != null
226            && inObj instanceof MatterImpl)
227      {
228         result = (0 == compareTo((MatterImpl) inObj));
229      }
230
231      return result;
232   }
233
234   //--------------------------------------------------------------------------
235   public int compareTo(MatterImpl inObj)
236   {
237      int result = -1;
238
239      if (inObj != null)
240      {
241         result = CompareUtil.compare(hashCode(), inObj.hashCode());
242      }
243
244      return result;
245   }
246
247   //--------------------------------------------------------------------------
248   public void setElementalComposition(Map<Element, Float> inMap)
249   {
250      mElementalComposition = null;
251      if (CollectionUtil.hasValues(inMap))
252      {
253         mElementalComposition = new HashMap<>(inMap);
254      }
255
256      clearCalculatedProperties();
257   }
258
259   //--------------------------------------------------------------------------
260   public void clearElementalComposition()
261   {
262      mElementalComposition = null;
263      clearCalculatedProperties();
264   }
265
266   //--------------------------------------------------------------------------
267   public MatterImpl addElementalComposition(Map<Element, Float> inMap)
268   {
269      return addElementalComposition(inMap, 1);
270   }
271
272   //--------------------------------------------------------------------------
273   public MatterImpl addElementalComposition(Map<Element, Float> inMap, int inNum)
274   {
275      if (CollectionUtil.hasValues(inMap))
276      {
277         for (Element element : inMap.keySet())
278         {
279            addAtoms(element, inMap.get(element) * inNum);
280         }
281
282         clearCalculatedProperties();
283      }
284
285      return this;
286   }
287
288   //--------------------------------------------------------------------------
289   public MatterImpl add(Matter inValue)
290   {
291      return add(inValue, 1);
292   }
293
294   //--------------------------------------------------------------------------
295   public MatterImpl add(Matter inValue, int inCount)
296   {
297      if (inValue != null)
298      {
299         // This will also clear calculated properties
300         addElementalComposition(inValue.getElementalComposition(), inCount);
301
302         // Now deal with user-set masses...
303
304         if (mMonoisotopicMassIsUserSet
305             || (null == inValue.getElementalComposition()
306                 && inValue.getMonoisotopicMass() != null))
307         {
308            setMonoisotopicMass((mMonoisotopicMassIsUserSet ? mMonoisotopicMass : CollectionUtil.hasValues(getElementalComposition()) ? getMonoisotopicMass() : 0.0)
309                  + (inValue.getMonoisotopicMass() != null ? inValue.getMonoisotopicMass() : 0) * inCount);
310         }
311
312         if (mAverageMassIsUserSet
313             || (null == inValue.getElementalComposition()
314                 && inValue.getAverageMass() != null))
315         {
316            setAverageMass((mAverageMassIsUserSet ? mAverageMass : CollectionUtil.hasValues(getElementalComposition()) ? getAverageMass() : 0.0)
317                  + (inValue.getAverageMass() != null ? inValue.getAverageMass() : 0) * inCount);
318         }
319      }
320
321      return this;
322   }
323
324   //--------------------------------------------------------------------------
325   public MatterImpl remove(Matter inValue)
326   {
327      return remove(inValue, 1);
328   }
329
330   //--------------------------------------------------------------------------
331   public MatterImpl remove(Matter inValue, int inCount)
332   {
333      return add(inValue, - inCount);
334   }
335
336   //--------------------------------------------------------------------------
337   public MatterImpl addAtoms(Element inElement, int inNum)
338   {
339      return addAtoms(inElement, new Float(inNum));
340   }
341
342   //--------------------------------------------------------------------------
343   public MatterImpl addAtoms(Element inElement, float inNum)
344   {
345      if (null == mElementalComposition)
346      {
347         mElementalComposition = new HashMap<Element, Float>();
348      }
349
350      Float count = mElementalComposition.get(inElement);
351      float newCount = inNum + (count != null ? count : 0);
352
353      mElementalComposition.put(inElement, newCount);
354
355      clearCalculatedProperties();
356
357      return this;
358   }
359
360   //--------------------------------------------------------------------------
361   /**
362    If the elemental composition is known, use setElementalComposition() and
363    the masses will be derived automatically; this method is for use in those
364    (hopefully) rare times when the mass is known but not the elemental composition.
365    @param inValue the mass to use as the monoisotopic mass for this object
366    @return this OrganicMatterImpl object to enable method chaining
367    */
368   public MatterImpl setMonoisotopicMass(Double inValue)
369   {
370      mMonoisotopicMass = inValue;
371      mMonoisotopicMassIsUserSet = (inValue != null);
372      return this;
373   }
374
375   //--------------------------------------------------------------------------
376   /**
377    If the elemental composition is known, use setElementalComposition() and
378    the masses will be derived automatically; this method is for use in those
379    (hopefully) rare times when the mass is known but not the elemental composition.
380    @param inValue the mass to use as the average mass for this object
381    @return this OrganicMatterImpl object to enable method chaining
382    */
383   public MatterImpl setAverageMass(Double inValue)
384   {
385      mAverageMass = inValue;
386      mAverageMassIsUserSet = (inValue != null);
387      return this;
388   }
389
390   //--------------------------------------------------------------------------
391   public Double getMonoisotopicMass()
392   {
393      if (null == mMonoisotopicMass)
394      {
395         calculateMassFromElementalComposition();
396      }
397
398      return mMonoisotopicMass;
399   }
400
401   //--------------------------------------------------------------------------
402   public Double getAverageMass()
403   {
404      if (null == mAverageMass)
405      {
406         calculateMassFromElementalComposition();
407      }
408
409      return mAverageMass;
410   }
411
412   //--------------------------------------------------------------------------
413   /**
414    Returns the elemental composition as an unmodifiable Map.
415    */
416   public Map<Element, Float> getElementalComposition()
417   {
418      return (mElementalComposition != null ? Collections.unmodifiableMap(mElementalComposition) : null);
419   }
420
421   //--------------------------------------------------------------------------
422   /**
423    Returns a chemical formula String like 'C5H11NO'. If carbon is present, it
424    is listed first followed by the other elements in ascending mass order.
425    Symbols for isotopes are enclosed in square brackets such as '[2H]2O'
426    for deuterated water.
427    @return the chemical formula string
428    */
429   public String getChemicalFormula()
430   {
431      return getChemicalFormula(false);
432   }
433
434   //--------------------------------------------------------------------------
435   /**
436    Returns a chemical formula String like 'C₅H₁₁NO'. If carbon is present, it
437    is listed first followed by the other elements in ascending mass order.
438    Symbols for isotopes are enclosed in square brackets such as '[²H]₂O'
439    for deuterated water.
440    @return the chemical formula string
441    */
442   public String getChemicalFormulaWithSubscripts()
443   {
444      return getChemicalFormula(true);
445   }
446
447   //--------------------------------------------------------------------------
448   private String getChemicalFormula(boolean inUseSubscripts)
449   {
450      StringBuilder compositionString = new StringBuilder();
451      Map<Element, Float> elemComp = getElementalComposition();
452      if (CollectionUtil.hasValues(elemComp))
453      {
454         List<Element> elements = new ArrayList<>(elemComp.keySet());
455         Collections.sort(elements);
456
457         if (elements.remove(Element.CARBON))
458         {
459            Float count = elemComp.get(Element.CARBON);
460            if (count != null && count != 0)
461            {
462               String countString = (count == count.intValue() ? count.intValue() + "" : String.format("%3.1f", count.floatValue()) + "");
463               if (inUseSubscripts)
464               {
465                  countString = StringUtil.toSubscript(countString);
466               }
467               compositionString.append("C" + (count == 1 ? "" : (count < 0 ? "(" + countString + ")" : countString)));
468            }
469         }
470
471         for (Element element : elements)
472         {
473            Float count = elemComp.get(element);
474            // Encode pure isotopes in square brackets like "[²H]" for deuterium
475            if (count != null && count != 0)
476            {
477               String countString = (count == count.intValue() ? count.intValue() + "" : String.format("%3.1f", count.floatValue()) + "");
478               String symbol = element.getSymbol();
479               if (inUseSubscripts)
480               {
481                  countString = StringUtil.toSubscript(countString);
482                  if (element instanceof Isotope)
483                  {
484                     symbol = StringUtil.toSuperscript(((Isotope) element).getMassNumber()) + ((Isotope) element).getElement().getSymbol();
485                  }
486               }
487
488               compositionString.append((element instanceof Isotope ? "[" + symbol + "]" : symbol)
489                     + (count == 1 ? "" : (count < 0 ? "(" + countString + ")" : countString)));
490            }
491         }
492      }
493
494      return compositionString.toString();
495   }
496
497
498   //--------------------------------------------------------------------------
499   public MatterImpl clone()
500   {
501      MatterImpl copy;
502      try
503      {
504         copy = (MatterImpl) super.clone();
505      }
506      catch (CloneNotSupportedException e)
507      {
508         throw new RuntimeException("Coding problem! CloneNotSupportedException should not be possible when cloning a "
509               + this.getClass().getSimpleName() + " object!", e);
510      }
511
512      if (mElementalComposition != null)
513      {
514         mElementalComposition = new HashMap<>(mElementalComposition);
515      }
516
517      return copy;
518   }
519
520
521   //--------------------------------------------------------------------------
522   public void clearCalculatedProperties()
523   {
524      if (! mMonoisotopicMassIsUserSet)   mMonoisotopicMass   = null;
525      if (! mAverageMassIsUserSet)        mAverageMass        = null;
526      mHashCode = null;
527   }
528
529   //##########################################################################
530   // PROTECTED METHODS
531   //##########################################################################
532
533   //--------------------------------------------------------------------------
534   protected void calculateMassFromElementalComposition()
535   {
536      Map<Element, Float> elementalCompositionMap = getElementalComposition();
537
538      if (elementalCompositionMap != null)
539      {
540         double mono       = 0.0;
541         double avg        = 0.0;
542
543         for (Element element : elementalCompositionMap.keySet())
544         {
545            float count = elementalCompositionMap.get(element);
546            mono += count * element.getMonoisotopicMass();
547
548            Double elementalAvgMass = element.getAverageMass();
549            avg += count * (elementalAvgMass != null ? elementalAvgMass : element.getMonoisotopicMass()); // If we don't have an avg. mass for the element, use monoisotopic
550         }
551
552         if (! mMonoisotopicMassIsUserSet)   mMonoisotopicMass   = new Double(mono);
553         if (! mAverageMassIsUserSet)        mAverageMass        = new Double(avg);
554      }
555   }
556
557   //--------------------------------------------------------------------------
558   protected boolean massesAreUserSet()
559   {
560      return (mMonoisotopicMassIsUserSet || mAverageMassIsUserSet);
561   }
562
563
564   //---------------------------------------------------------------------------
565   // Used when parsing chemical formulas
566   private static Character convertSubscriptChar(char inChar)
567   {
568      Character subscriptChar = null;
569      switch (inChar)
570      {
571         case '\u2080':
572            subscriptChar = '0';
573            break;
574         case '\u2081':
575            subscriptChar = '1';
576            break;
577         case '\u2082':
578            subscriptChar = '2';
579            break;
580         case '\u2083':
581            subscriptChar = '3';
582            break;
583         case '\u2084':
584            subscriptChar = '4';
585            break;
586         case '\u2085':
587            subscriptChar = '5';
588            break;
589         case '\u2086':
590            subscriptChar = '6';
591            break;
592         case '\u2087':
593            subscriptChar = '7';
594            break;
595         case '\u2088':
596            subscriptChar = '8';
597            break;
598         case '\u2089':
599            subscriptChar = '9';
600            break;
601      }
602
603      return subscriptChar;
604   }
605
606   //---------------------------------------------------------------------------
607   // Used when parsing isotopes in chemical formulas
608   private static Character convertSuperscriptChar(char inChar)
609   {
610      Character superscriptChar = null;
611      switch (inChar)
612      {
613         case '\u207B':
614            superscriptChar = '-';
615            break;
616         case '\u2070':
617            superscriptChar = '0';
618            break;
619         case 0xB9:
620            superscriptChar = '1';
621            break;
622         case 0xB2:
623            superscriptChar = '2';
624            break;
625         case 0xB3:
626            superscriptChar = '3';
627            break;
628         case '\u2074':
629            superscriptChar = '4';
630            break;
631         case '\u2075':
632            superscriptChar = '5';
633            break;
634         case '\u2076':
635            superscriptChar = '6';
636            break;
637         case '\u2077':
638            superscriptChar = '7';
639            break;
640         case '\u2078':
641            superscriptChar = '8';
642            break;
643         case '\u2079':
644            superscriptChar = '9';
645            break;
646      }
647
648      return superscriptChar;
649   }
650
651
652   //##########################################################################
653   // PRIVATE CLASS
654   //##########################################################################
655
656
657   private class FormulaSubBlock
658   {
659      private StringBuilder mString = new StringBuilder();
660      private int mCount = 1;
661      private char mBracketType;
662      private boolean mClosed = false;
663      List<MatterImpl.FormulaSubBlock> mSubBlocks;
664
665      //-----------------------------------------------------------------------
666      public String toString()
667      {
668         return mString.toString();
669      }
670
671      //-----------------------------------------------------------------------
672      public void append(char inChar)
673      {
674         mString.append(inChar);
675      }
676
677      //-----------------------------------------------------------------------
678      public void setCount(int inValue)
679      {
680         mCount = inValue;
681      }
682
683      //-----------------------------------------------------------------------
684      public FormulaSubBlock setBracketType(char inValue)
685      {
686         mBracketType = inValue;
687         return this;
688      }
689
690      //-----------------------------------------------------------------------
691      public void close()
692      {
693         mClosed = true;
694      }
695
696      //-----------------------------------------------------------------------
697      public boolean isClosed()
698      {
699         return mClosed;
700      }
701
702      //-----------------------------------------------------------------------
703      public void addSubBlock(MatterImpl.FormulaSubBlock inValue)
704      {
705         if (null == mSubBlocks)
706         {
707            mSubBlocks = new ArrayList<>(4);
708         }
709
710         mSubBlocks.add(inValue);
711      }
712
713      //-----------------------------------------------------------------------
714      public Map<Element, Float> getChemicalComposition()
715      {
716         Map<Element, Float> chemicalCompositionMap = getChemicalComposition(mString.toString());
717
718         if (CollectionUtil.hasValues(mSubBlocks))
719         {
720            for (MatterImpl.FormulaSubBlock subBlock : mSubBlocks)
721            {
722               Map<Element, Float> subBlockCompositionMap = subBlock.getChemicalComposition();
723               for (Element element : subBlockCompositionMap.keySet())
724               {
725                  float updatedCount = subBlockCompositionMap.get(element);
726
727                  Float existingCount = chemicalCompositionMap.get(element);
728                  if (existingCount != null)
729                  {
730                     updatedCount += existingCount;
731                  }
732
733                  chemicalCompositionMap.put(element, updatedCount);
734               }
735            }
736         }
737
738         return chemicalCompositionMap;
739      }
740
741      //-----------------------------------------------------------------------
742      private Map<Element, Float> getChemicalComposition(String inString)
743      {
744         Map<Element, Float> chemicalCompositionMap = new HashMap<>(10);
745
746         int i = 0;
747         char prevChar = ' ';
748         while (i < inString.length())
749         {
750            char currentChar = inString.charAt(i);
751
752            if (currentChar == ' '     // Skip whitespace
753                  || currentChar == ':'  // Skip bond notation
754                  || (currentChar == '-' && ! Character.isDigit(prevChar))  // Skip linear formula single bond notation
755                  || currentChar == '−'  // Skip linear formula single bond notation
756                  || currentChar == '='  // Skip linear formula double bond notation
757                  || currentChar == '≡'  // Skip linear formula triple bond notation
758                  || currentChar == '@') // Skip trapped atom notation
759            {
760               i++;
761               continue;
762            }
763
764            if (mBracketType == '['
765                  && 0 == i
766                  && (Character.isDigit(currentChar)
767                      || CharUtil.isSuperscript(currentChar)))
768            {
769               // Isotope
770
771               String massNumString = "";
772               Character convertedSuperscriptChar = null;
773               while (i < inString.length())
774               {
775                  char theChar = inString.charAt(i);
776                  if (Character.isDigit(theChar)
777                        || (convertedSuperscriptChar = convertSuperscriptChar(theChar)) != null)
778                  {
779                     massNumString += (convertedSuperscriptChar != null ? convertedSuperscriptChar : theChar);
780                     i++;
781                  }
782                  else
783                  {
784                     break;
785                  }
786               }
787
788               Element element = null;
789               if (i < inString.length() - 1)
790               {
791                  element = Element.valueOf(inString.substring(i, i + 2));
792               }
793
794               if (element != null)
795               {
796                  i += 2;
797               }
798               else
799               {
800                  element = Element.valueOf(inString.substring(i, i + 1));
801                  if (element != null)
802                  {
803                     i++;
804                  }
805                  else
806                  {
807                     throw new ChemicalFormulaParseException("Problem parsing elements from " + StringUtil.singleQuote(inString) + " at char " + (i + 1) + "!");
808                  }
809               }
810
811               Isotope isotope = Isotope.valueOf(element, Integer.parseInt(massNumString));
812
813               float updatedCount = mCount;
814
815               Float existingCount = chemicalCompositionMap.get(isotope);
816               if (existingCount != null)
817               {
818                  updatedCount += existingCount;
819               }
820
821               chemicalCompositionMap.put(isotope, updatedCount);
822            }
823            else if (sSaltIndicatorsInChemicalFormulas.contains(currentChar))
824            {
825               // molecule of crystallization
826               i++;
827               String countString = "";
828               while (i < inString.length())
829               {
830                  char countChar = inString.charAt(i);
831
832                  if (countChar != ' ')     // Skip whitespace
833                  {
834                     if (Character.isDigit(countChar))
835                     {
836                        countString += countChar;
837                     }
838                     else
839                     {
840                        break;
841                     }
842                  }
843
844                  i++;
845               }
846
847               float count = StringUtil.isSet(countString) ? Float.parseFloat(countString) : 1;
848
849               Map<Element, Float> crystallizationChemicalCompositionMap = getChemicalComposition(inString.substring(i));
850               for (Element element : crystallizationChemicalCompositionMap.keySet())
851               {
852                  float updatedCount = crystallizationChemicalCompositionMap.get(element) * count * mCount;
853
854                  Float existingCount = chemicalCompositionMap.get(element);
855                  if (existingCount != null)
856                  {
857                     updatedCount += existingCount;
858                  }
859
860                  chemicalCompositionMap.put(element, updatedCount);
861               }
862
863               // There shouldn't be anything after the crystallization molecule
864               break;
865            }
866            else
867            {
868               Element element = null;
869               if (i < inString.length() - 1)
870               {
871                  element = Element.valueOf(inString.substring(i, i + 2));
872               }
873
874               if (element != null)
875               {
876                  i += 2;
877               }
878               else
879               {
880                  element = Element.valueOf(inString.substring(i, i + 1));
881                  if (element != null)
882                  {
883                     i++;
884                  }
885                  else
886                  {
887                     throw new ChemicalFormulaParseException("Problem parsing elements from " + StringUtil.singleQuote(inString) + " at char " + (i + 1) + "!");
888                  }
889               }
890
891               String countString = "";
892               Character convertedSubscriptChar = null;
893               while (i < inString.length())
894               {
895                  char countChar = inString.charAt(i);
896                  if (Character.isDigit(countChar)
897                      || (convertedSubscriptChar = convertSubscriptChar(countChar)) != null)
898                  {
899                     countString += (convertedSubscriptChar != null ? convertedSubscriptChar : countChar);
900                     i++;
901                  }
902                  else if ('-' == countChar)
903                  {
904                     throw new ChemicalFormulaParseException("Problem parsing elements from " + StringUtil.singleQuote(inString) + " at char " + (i + 1) + ": Counts can't be ranges!");
905                  }
906                  else
907                  {
908                     break;
909                  }
910               }
911
912               float updatedCount = mCount * (StringUtil.isSet(countString) ? Float.parseFloat(countString) : 1);
913
914               Float existingCount = chemicalCompositionMap.get(element);
915               if (existingCount != null)
916               {
917                  updatedCount += existingCount;
918               }
919
920               chemicalCompositionMap.put(element, updatedCount);
921            }
922
923            prevChar = currentChar;
924
925         }
926
927         return chemicalCompositionMap;
928      }
929   }
930}