class Compound(BaseModel):
    names = ListType(StringType())
    labels = ListType(StringType())
    roles = ListType(StringType())
    nmr_spectra = ListType(ModelType(NmrSpectrum))
    ir_spectra = ListType(ModelType(IrSpectrum))
    uvvis_spectra = ListType(ModelType(UvvisSpectrum))
    melting_points = ListType(ModelType(MeltingPoint))
    glass_transitions = ListType(ModelType(GlassTransition))
    quantum_yields = ListType(ModelType(QuantumYield))
    fluorescence_lifetimes = ListType(ModelType(FluorescenceLifetime))
    electrochemical_potentials = ListType(ModelType(ElectrochemicalPotential))

    def merge(self, other):
        """Merge data from another Compound into this Compound."""
        log.debug('Merging: %s and %s' % (self.serialize(), other.serialize()))
        for k in self.keys():
            for new_item in other[k]:
                if new_item not in self[k]:
                    self[k].append(new_item)
        log.debug('Result: %s' % self.serialize())
        return self

    def merge_contextual(self, other):
        """Merge in contextual info from a template Compound."""
        # TODO: This is currently dependent on our data model? Make more robust to schema changes
        # Currently we assume all lists at Compound level, with 1 further potential nested level of lists
        for k in self.keys():
            # print('key: %s' % k)
            for item in self[k]:
                # print('item: %s' % item)
                for other_item in other.get(k, []):
                    # Skip text properties (don't merge names, labels, roles)
                    if isinstance(other_item, six.text_type):
                        continue
                    for otherk in other_item.keys():
                        if isinstance(other_item[otherk], list):
                            if len(other_item[otherk]) > 0 and len(item[otherk]) > 0:
                                other_nested_item = other_item[otherk][0]
                                for othernestedk in other_nested_item.keys():
                                    for nested_item in item[otherk]:
                                        if not nested_item[othernestedk]:
                                            nested_item[othernestedk] = other_nested_item[othernestedk]
                        elif not item[otherk]:
                            item[otherk] = other_item[otherk]
        log.debug('Result: %s' % self.serialize())
        return self

    @property
    def is_unidentified(self):
        if not self.names and not self.labels:
            return True
        return False

    # @property
    # def is_contextual(self):
    #     for k in self:
    #         # Not contextual if we have any names or labels
    #         if k in {'names', 'labels'}:
    #             return False
    #     return super(Compound, self).is_contextual


    @property
    def is_id_only(self):
        """Return True if identifier information only."""
        for key, value in self.items():
            if key not in {'names', 'labels', 'roles'} and value:
                return False
        if self.names or self.labels:
            return True
        return False class IrSpectrum(BaseModel):
    solvent = StringType(contextual=True)
    temperature = StringType(contextual=True)
    temperature_units = StringType(contextual=True)
    concentration = StringType(contextual=True)
    concentration_units = StringType(contextual=True)
    apparatus = StringType(contextual=True)
    peaks = ListType(ModelType(IrPeak)) class IrPeak(BaseModel):
    value = StringType()
    units = StringType(contextual=True)
    strength = StringType()
    bond = StringType() class BaseParser(object):
    """"""

    @abstractproperty
    def root(self):
        pass

    @abstractmethod
    def interpret(self, result, start, end):
        pass

    def parse(self, tokens):
        for result in self.root.scan(tokens):
            for model in self.interpret(*result):
                yield model def first(el):
    if len(el) > 0:
        return el[0]
    else:
        return None def join(tokens, start, result):
    """Join tokens into a single string with spaces between."""
    texts = []
    if len(result) > 0:
        for e in result:
            for child in e.iter():
                if child.text is not None:
                    texts.append(child.text)
        return [E(result[0].tag, ' '.join(texts))] def merge(tokens, start, result):
    """Join tokens into a single string with no spaces."""
    texts = []
    if len(result) > 0:
        for e in result:
            for child in e.iter():
                if child.text is not None:
                    texts.append(child.text)
        return [E(result[0].tag, ''.join(texts))] def strip_stop(tokens, start, result):
    """Remove trailing full stop from tokens."""
    for e in result:
        for child in e.iter():
            if child.text.endswith('.'):
                child.text = child.text[:-1]
    return result W = Word I = IWord T = Tag R = Regex class Optional(ParseElementEnhance):

    def __init__(self, expr):
        super(Optional, self).__init__(expr)

    def _parse_tokens(self, tokens, i, actions=True):
        results = []
        try:
            results, i = self.expr.parse(tokens, i, actions)
        except (ParseException, IndexError):
            pass
        return results, i class ZeroOrMore(ParseElementEnhance):
    """Optional repetition of zero or more of the given expression."""

    def _parse_tokens(self, tokens, i, actions=True):
        results = []
        try:
            results, i = self.expr.parse(tokens, i, actions)
            while 1:
                start_i = i
                tmpresults, i = self.expr.parse(tokens, start_i, actions)
                if tmpresults:
                    results.extend(tmpresults)
        except (ParseException, IndexError):
            pass
        return ([E(self.name, *results)] if self.name else results), i class OneOrMore(ParseElementEnhance):
    """Repetition of one or more of the given expression."""

    def _parse_tokens(self, tokens, i, actions=True):
        # must be at least one
        results, i = self.expr.parse(tokens, i, actions)
        try:
            while 1:
                start_i = i
                tmpresults, i = self.expr.parse(tokens, start_i, actions)
                if tmpresults:
                    results.extend(tmpresults)
        except (ParseException, IndexError):
            pass
        return ([E(self.name, *results)] if self.name else results), i class Not(ParseElementEnhance):
    """Check ahead to disallow a match with the given parse expression."""

    def _parse_tokens(self, tokens, i, actions=True):
        try:
            self.expr.try_parse(tokens, i)
        except (ParseException, IndexError):
            pass
        else:
            raise ParseException(tokens, i, 'Encountered disallowed token', self)
        return [], i
import logging
import re
from lxml.builder import E
from ..model import Compound, IrSpectrum, IrPeak
from .base import BaseParser
from ..utils import first
from .actions import join, merge, strip_stop
from .common import hyphen
from .elements import W, I, T, R, Optional, ZeroOrMore, OneOrMore, Not
from .cem import chemical_name
# -*- coding: utf-8 -*-
"""
chemdataextractor.parse.ir
~~~~~~~~~~~~~~~~~~~~~~~~~~

IR spectrum text parser.

"""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals




log = logging.getLogger(__name__)


def extract_units(tokens, start, result):
    """Extract units from bracketed after nu"""
    for e in result:
        for child in e.iter():
            if 'cm−1' in child.text:
                return [E('units', 'cm−1')]
    return []


delim = R('^[;:,\./]$').hide()

# Not really just solvent, also nujol suspension, pellet, or ATR
ir_solvent = (I('KBr') | I('ATR') | I('neat') | I('NaCl') | I('CaF2') | I('AgCl') | I('CsI')) + Optional(I('pellet'))

solvent = (ir_solvent | chemical_name)('solvent').add_action(join)

units = Optional(W('/')).hide() + (
    R('^\[?cm[-–−‒]?1\]?$') |
    W('cm') + R('^[-–−‒]$') + W('1')
)('units').add_action(merge)


value_range = ((R('^\d{,2}[ ,]?\d{3}(\.\d+)?[\-–−‒]\d{,2}[ ,]?\d{3}(\.\d+)?$') | (R('^\d{,2}[ ,]?\d{3}(\.\d+)?$') + R('^[\-–−‒]$') + R('^\d{,2}[ ,]?\d{3}(\.\d+)?$'))))('value').add_action(merge)
value = R('^\d{,2}[ ,]?\d{3}(\.\d+)?\.?$')('value').add_action(strip_stop)

strength = R('^(m|medium|w|weak|s|strong|n|narrow|b|broad|sh|sharp)$', re.I)('strength') + Optional(I('peak')).hide()

bond = OneOrMore(Not(W(')')) + (T('B-CM') | T('I-CM') | T('JJ') | T('NN') | T('NNP') | T('NNS') | T('HYPH') | T('CD') | T('LS') | T('CC')))('bond').add_action(join)

peak_meta_options = strength | bond

peak_meta = W('(').hide() + peak_meta_options + ZeroOrMore(delim + peak_meta_options) + W(')').hide()

nu = (R('^[vνυ]̃?(max)?(\(cm−1\))?$') + Optional(W('max')) + Optional(W('='))).add_action(extract_units)

spectrum_meta = W('(').hide() + (units | solvent | nu) + ZeroOrMore(delim + (units | solvent | nu)) + W(')').hide()


insolvent = T('IN') + solvent

ir_type = (Optional(W('FT') + hyphen) + R('^(FT-?)?IR|FT-?IS|IR-ATR$') + Optional(hyphen + W('ATR')))('type').add_action(merge)


prelude = (ir_type | R('^[vνυ]max$').hide()) + Optional(I('data')) + Optional(insolvent) + ZeroOrMore(spectrum_meta) + Optional(delim) + Optional(nu) + Optional(delim) + Optional(units)