1# -*- coding: utf-8 -*-
2# ------------------------------------------------------------------------------
3# Name:         romanText/rtObjects.py
4# Purpose:      music21 objects for processing roman numeral analysis text files
5#
6# Authors:      Christopher Ariza
7#               Michael Scott Cuthbert
8#
9# Copyright:    Copyright © 2011-2012, 2019 Michael Scott Cuthbert and the music21 Project
10# License:      BSD, see license.txt
11# ------------------------------------------------------------------------------
12'''
13Translation routines for roman numeral analysis text files, as defined
14and demonstrated by Dmitri Tymoczko, Mark Gotham, Michael Scott Cuthbert,
15and Christopher Ariza in ISMIR 2019.
16'''
17import fractions
18import io
19import re
20import unittest
21
22from music21 import common
23from music21 import exceptions21
24from music21 import environment
25from music21 import key
26from music21 import prebase
27
28_MOD = 'romanText.rtObjects'
29environLocal = environment.Environment(_MOD)
30
31# alternate endings might end with a, b, c for non
32# zero or more for everything after the first number
33reMeasureTag = re.compile(r'm[0-9]+[a-b]*-*[0-9]*[a-b]*')
34reVariant = re.compile(r'var[0-9]+')
35reVariantLetter = re.compile(r'var([A-Z]+)')
36
37reOptKeyOpenAtom = re.compile(r'\?\([A-Ga-g]+[b#]*:')
38reOptKeyCloseAtom = re.compile(r'\?\)[A-Ga-g]+[b#]*:?')
39# ?g:( ?
40reKeyAtom = re.compile('[A-Ga-g]+[b#]*;:')
41reAnalyticKeyAtom = re.compile('[A-Ga-g]+[b#]*:')
42reKeySignatureAtom = re.compile(r'KS-?[0-7]')
43# must distinguish b3 from bVII; there may be b1.66.5
44reBeatAtom = re.compile(r'b[1-9.]+')
45reRepeatStartAtom = re.compile(r'\|\|:')
46reRepeatStopAtom = re.compile(r':\|\|')
47reNoChordAtom = re.compile('(NC|N.C.|nc)')
48
49
50# ------------------------------------------------------------------------------
51
52class RomanTextException(exceptions21.Music21Exception):
53    pass
54
55
56class RTTokenException(exceptions21.Music21Exception):
57    pass
58
59
60class RTHandlerException(exceptions21.Music21Exception):
61    pass
62
63
64class RTFileException(exceptions21.Music21Exception):
65    pass
66
67
68# ------------------------------------------------------------------------------
69
70class RTToken(prebase.ProtoM21Object):
71    '''Stores each linear, logical entity of a RomanText.
72
73    A multi-pass parsing procedure is likely necessary, as RomanText permits
74    variety of groupings and markings.
75
76    >>> rtt = romanText.rtObjects.RTToken('||:')
77    >>> rtt
78    <music21.romanText.rtObjects.RTToken '||:'>
79
80    A standard RTToken returns `False` for all of the following.
81
82    >>> rtt.isComposer() or rtt.isTitle() or rtt.isPiece()
83    False
84    >>> rtt.isAnalyst() or rtt.isProofreader()
85    False
86    >>> rtt.isTimeSignature() or rtt.isKeySignature() or rtt.isNote()
87    False
88    >>> rtt.isForm() or rtt.isPedal() or rtt.isMeasure() or rtt.isWork()
89    False
90    >>> rtt.isMovement() or rtt.isVersion() or rtt.isAtom()
91    False
92    '''
93
94    def __init__(self, src=''):
95        self.src = src  # store source character sequence
96        self.lineNumber = 0
97
98    def _reprInternal(self):
99        return repr(self.src)
100
101    def isComposer(self):
102        return False
103
104    def isTitle(self):
105        return False
106
107    def isPiece(self):
108        return False
109
110    def isAnalyst(self):
111        return False
112
113    def isProofreader(self):
114        return False
115
116    def isTimeSignature(self):
117        return False
118
119    def isKeySignature(self):
120        return False
121
122    def isNote(self):
123        return False
124
125    def isForm(self):
126        '''Occasionally found in header.
127        '''
128        return False
129
130    def isMeasure(self):
131        return False
132
133    def isPedal(self):
134        return False
135
136    def isWork(self):
137        return False
138
139    def isMovement(self):
140        return False
141
142    def isVersion(self):
143        return False
144
145    def isAtom(self):
146        '''Atoms are any untagged data; generally only found inside of a
147        measure definition.
148        '''
149        return False
150
151
152class RTTagged(RTToken):
153    '''In romanText, some data elements are tags, that is a tag name, a colon,
154    optional whitespace, and data. In non-RTTagged elements, there is just
155    data.
156
157    All tagged tokens are subclasses of this class. Examples are:
158
159        Title: Die Jahrzeiten
160        Composer: Fanny Mendelssohn
161
162    >>> rtTag = romanText.rtObjects.RTTagged('Title: Die Jahrzeiten')
163    >>> rtTag.tag
164    'Title'
165    >>> rtTag.data
166    'Die Jahrzeiten'
167    >>> rtTag.isTitle()
168    True
169    >>> rtTag.isComposer()
170    False
171    '''
172
173    def __init__(self, src=''):
174        super().__init__(src)
175        # try to split off tag from data
176        self.tag = ''
177        self.data = ''
178        if ':' in src:
179            iFirst = src.find(':')  # first index found at
180            self.tag = src[:iFirst].strip()
181            # add one to skip colon
182            self.data = src[iFirst + 1:].strip()
183        else:  # we do not have a clear tag; perhaps store all as data
184            self.data = src
185
186    def isComposer(self):
187        '''True is the tag represents a composer.
188
189        >>> rth = romanText.rtObjects.RTTagged('Composer: Claudio Monteverdi')
190        >>> rth.isComposer()
191        True
192        >>> rth.isTitle()
193        False
194        >>> rth.isWork()
195        False
196        >>> rth.data
197        'Claudio Monteverdi'
198        '''
199        if self.tag.lower() == 'composer':
200            return True
201        return False
202
203    def isTitle(self):
204        '''True if tag represents a title, otherwise False.
205
206        >>> tag = romanText.rtObjects.RTTagged('Title: This is a title.')
207        >>> tag.isTitle()
208        True
209
210        >>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.')
211        >>> tag.isTitle()
212        False
213        '''
214        if self.tag.lower() == 'title':
215            return True
216        return False
217
218    def isPiece(self):
219        '''
220        True if tag represents a piece, otherwise False.
221
222        >>> tag = romanText.rtObjects.RTTagged('Piece: This is a piece.')
223        >>> tag.isPiece()
224        True
225
226        >>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.')
227        >>> tag.isPiece()
228        False
229        '''
230        if self.tag.lower() == 'piece':
231            return True
232        return False
233
234    def isAnalyst(self):
235        '''True if tag represents a analyst, otherwise False.
236
237        >>> tag = romanText.rtObjects.RTTagged('Analyst: This is an analyst.')
238        >>> tag.isAnalyst()
239        True
240
241        >>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.')
242        >>> tag.isAnalyst()
243        False
244        '''
245        if self.tag.lower() == 'analyst':
246            return True
247        return False
248
249    def isProofreader(self):
250        '''True if tag represents a proofreader, otherwise False.
251
252        >>> tag = romanText.rtObjects.RTTagged('Proofreader: This is a proofreader.')
253        >>> tag.isProofreader()
254        True
255
256        >>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.')
257        >>> tag.isProofreader()
258        False
259        '''
260        if self.tag.lower() in ('proofreader', 'proof reader'):
261            return True
262        return False
263
264    def isTimeSignature(self):
265        '''True if tag represents a time signature, otherwise False.
266
267        >>> tag = romanText.rtObjects.RTTagged('TimeSignature: This is a time signature.')
268        >>> tag.isTimeSignature()
269        True
270
271        >>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.')
272        >>> tag.isTimeSignature()
273        False
274
275        TimeSignature header data can be found intermingled with measures.
276        '''
277        if self.tag.lower() in ('timesignature', 'time signature'):
278            return True
279        return False
280
281    def isKeySignature(self):
282        '''
283        True if tag represents a key signature, otherwise False.
284
285        >>> tag = romanText.rtObjects.RTTagged('KeySignature: This is a key signature.')
286        >>> tag.isKeySignature()
287        True
288        >>> tag.data
289        'This is a key signature.'
290
291        KeySignatures are a type of tagged data found outside of measures,
292        such as "Key Signature: -1" meaning one flat.
293
294        Key signatures are generally numbers representing the number of sharps (or
295        negative for flats).  Non-standard key signatures are not supported.
296
297        >>> tag = romanText.rtObjects.RTTagged('KeySignature: -3')
298        >>> tag.data
299        '-3'
300
301        music21 supports one legacy key signature type: `KeySignature: Bb` which
302        represents a one-flat signature.  Important to note: no other key signatures
303        of this type are supported.  (For instance, `KeySignature: Ab` has no effect)
304
305        >>> tag = romanText.rtObjects.RTTagged('KeySignature: Bb')
306        >>> tag.data
307        'Bb'
308
309        Testing that `.isKeySignature` returns `False` for non-key signatures:
310
311        >>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.')
312        >>> tag.isKeySignature()
313        False
314
315
316        N.B.: this is not the same as a key definition found inside of a
317        Measure. These are represented by RTKey rtObjects, defined below, and are
318        not RTTagged rtObjects, but RTAtom subclasses.
319        '''
320        if self.tag.lower() in ('keysignature', 'key signature'):
321            return True
322        else:
323            return False
324
325    def isNote(self):
326        '''True if tag represents a note, otherwise False.
327
328        >>> tag = romanText.rtObjects.RTTagged('Note: This is a note.')
329        >>> tag.isNote()
330        True
331
332        >>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.')
333        >>> tag.isNote()
334        False
335        '''
336        if self.tag.lower() == 'note':
337            return True
338        return False
339
340    def isForm(self):
341        '''True if tag represents a form, otherwise False.
342
343        >>> tag = romanText.rtObjects.RTTagged('Form: This is a form.')
344        >>> tag.isForm()
345        True
346
347        >>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.')
348        >>> tag.isForm()
349        False
350        '''
351        if self.tag.lower() == 'form':
352            return True
353        return False
354
355    def isPedal(self):
356        '''True if tag represents a pedal, otherwise False.
357
358        >>> tag = romanText.rtObjects.RTTagged('Pedal: This is a pedal.')
359        >>> tag.isPedal()
360        True
361
362        >>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.')
363        >>> tag.isPedal()
364        False
365        '''
366        if self.tag.lower() in ['pedal']:
367            return True
368        return False
369
370    def isVersion(self):
371        '''True if tag defines the version of RomanText standard used,
372        otherwise False.
373
374        Pieces without the tag are defined to conform to RomanText 1.0,
375        the version described in the ISMIR publication.
376
377        >>> rth = romanText.rtObjects.RTTagged('RTVersion: 1.0')
378        >>> rth.isTitle()
379        False
380        >>> rth.isVersion()
381        True
382        >>> rth.tag
383        'RTVersion'
384        >>> rth.data
385        '1.0'
386        '''
387        if self.tag.lower() == 'rtversion':
388            return True
389        else:
390            return False
391
392    def isWork(self):
393        '''True if tag represents a work, otherwise False.
394
395        The "work" is not defined as a header tag, but is used to represent
396        all tags, often placed after Composer, for the work or pieces designation.
397
398        >>> rth = romanText.rtObjects.RTTagged('Work: BWV232')
399        >>> rth.isWork()
400        True
401        >>> rth.tag
402        'Work'
403        >>> rth.data
404        'BWV232'
405
406        For historical reasons, the tag 'Madrigal' also designates a work.
407
408        >>> rth = romanText.rtObjects.RTTagged('Madrigal: 4.12')
409        >>> rth.isTitle()
410        False
411        >>> rth.isWork()
412        True
413        >>> rth.tag
414        'Madrigal'
415        >>> rth.data
416        '4.12'
417        '''
418        if self.tag.lower() in ('work', 'madrigal'):
419            return True
420        else:
421            return False
422
423    def isMovement(self):
424        '''True if tag represents a movement, otherwise False.
425
426        >>> tag = romanText.rtObjects.RTTagged('Movement: This is a movement.')
427        >>> tag.isMovement()
428        True
429
430        >>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.')
431        >>> tag.isMovement()
432        False
433        '''
434        if self.tag.lower() == 'movement':
435            return True
436        return False
437
438    def isSixthMinor(self):
439        '''
440        True if tag represents a configuration setting for setting vi/vio/VI in minor
441
442        >>> tag = romanText.rtObjects.RTTagged('Sixth Minor: Flat')
443        >>> tag.isSixthMinor()
444        True
445        >>> tag.data
446        'Flat'
447        '''
448        return self.tag.lower() in ('sixthminor', 'sixth minor')  # e.g. 'Sixth Minor: FLAT'
449
450    def isSeventhMinor(self):
451        '''
452        True if tag represents a configuration setting for setting vii/viio/VII in minor
453
454        >>> tag = romanText.rtObjects.RTTagged('Seventh Minor: Courtesy')
455        >>> tag.isSeventhMinor()
456        True
457        >>> tag.data
458        'Courtesy'
459        '''
460        return self.tag.lower() in ('seventhminor', 'seventh minor')
461
462
463class RTMeasure(RTToken):
464    '''In RomanText, measures are given one per line and always start with 'm'.
465
466    For instance:
467
468        m4 i b3 v b4 VI
469        m5 b2 g: IV b4 V
470        m6 i
471        m7 D: V
472
473    Measure ranges can be used and copied, such as:
474
475        m8-m9=m4-m5
476
477    RTMeasure objects can also define variant readings for a measure:
478
479        m1     ii
480        m1var1 ii b2 ii6 b3 IV
481
482    Variants are not part of the tag, but are read into an attribute.
483
484    Endings are indicated by a single letter after the measure number, such as
485    "a" for first ending.
486
487    >>> rtm = romanText.rtObjects.RTMeasure('m15a V6 b1.5 V6/5 b2 I b3 viio6')
488    >>> rtm.data
489    'V6 b1.5 V6/5 b2 I b3 viio6'
490    >>> rtm.number
491    [15]
492    >>> rtm.repeatLetter
493    ['a']
494    >>> rtm.isMeasure()
495    True
496
497
498    '''
499
500    def __init__(self, src=''):
501        super().__init__(src)
502        # try to split off tag from data
503        self.tag = ''  # the measure number or range
504        self.data = ''  # only chord, phrase, and similar definitions
505        self.number = []  # one or more measure numbers
506        self.repeatLetter = []  # one or more repeat letters
507        self.variantNumber = None  # a one-measure or short variant
508        # a longer-variant that
509        # defines a different way of reading a large section
510        self.variantLetter = None
511        # store boolean if this measure defines copying another range
512        self.isCopyDefinition = False
513        # store processed tokens associated with this measure
514        self.atoms = []
515
516        if src:
517            self._parseAttributes(src)
518
519    def _getMeasureNumberData(self, src):
520        '''Return the number or numbers as a list, as well as any repeat
521        indications.
522
523        >>> rtm = romanText.rtObjects.RTMeasure()
524        >>> rtm._getMeasureNumberData('m77')
525        ([77], [''])
526        >>> rtm._getMeasureNumberData('m123b-432b')
527        ([123, 432], ['b', 'b'])
528        '''
529        # note: this is separate procedure b/c it is used to get copy
530        # boundaries
531        if '-' in src:  # its a range
532            mnStart, mnEnd = src.split('-')
533            proc = [mnStart, mnEnd]
534        else:
535            proc = [src]  # treat as one
536        number = []
537        repeatLetter = []
538        for mn in proc:
539            # append in order, start, end
540            numStr, alphaStr = common.getNumFromStr(mn)
541            number.append(int(numStr))
542            # remove all 'm' in alpha
543            alphaStr = alphaStr.replace('m', '')
544            repeatLetter.append(alphaStr)
545        return number, repeatLetter
546
547    def _parseAttributes(self, src):
548        # assume that we have already checked that this is a measure
549        g = reMeasureTag.match(src)
550        if g is None:  # not measure tag found
551            raise RTHandlerException(f'found no measure tag: {src}')
552        iEnd = g.end()  # get end index
553        rawTag = src[:iEnd].strip()
554        self.tag = rawTag
555        rawData = src[iEnd:].strip()  # may have variant
556
557        # get the number list from the tag
558        self.number, self.repeatLetter = self._getMeasureNumberData(rawTag)
559
560        # strip a variant indication off of rawData if found
561        g = reVariant.match(rawData)
562        if g is not None:  # there is a variant tag
563            varStr = g.group(0)
564            self.variantNumber = int(common.getNumFromStr(varStr)[0])
565            self.data = rawData[g.end():].strip()
566        else:
567            self.data = rawData
568        g = reVariantLetter.match(rawData)
569        if g is not None:  # there is a variant letter tag
570            varStr = g.group(1)
571            self.variantLetter = varStr
572            self.data = rawData[g.end():].strip()
573
574        if self.data.startswith('='):
575            self.isCopyDefinition = True
576
577    def _reprInternal(self):
578        if len(self.number) == 1:
579            numberStr = str(self.number[0])
580        else:
581            numberStr = f'{self.number[0]}-{self.number[1]}'
582        return numberStr
583
584    def isMeasure(self):
585        return True
586
587    def getCopyTarget(self):
588        '''If this measure defines a copy operation, return two lists defining
589        the measures to copy; the second list has the repeat data.
590
591        >>> rtm = romanText.rtObjects.RTMeasure('m35-36 = m29-30')
592        >>> rtm.number
593        [35, 36]
594        >>> rtm.getCopyTarget()
595        ([29, 30], ['', ''])
596
597        >>> rtm = romanText.rtObjects.RTMeasure('m4 = m1')
598        >>> rtm.number
599        [4]
600        >>> rtm.getCopyTarget()
601        ([1], [''])
602        '''
603        # remove equal sign
604        rawData = self.data.replace('=', '').strip()
605        return self._getMeasureNumberData(rawData)
606
607
608class RTAtom(RTToken):
609    '''In RomanText, definitions of chords, phrases boundaries, open/close
610    parenthesis, beat indicators, etc. appear within measures (RTMeasure
611    objects). These individual elements will be called Atoms, as they are data
612    that is not tagged.
613
614    Each atom store a reference to its container (normally an RTMeasure).
615
616    >>> chordIV = romanText.rtObjects.RTAtom('IV')
617    >>> beat4 = romanText.rtObjects.RTAtom('b4')
618    >>> beat4
619    <music21.romanText.rtObjects.RTAtom 'b4'>
620    >>> beat4.isAtom()
621    True
622
623    However, see RTChord, RTBeat, etc. which are subclasses of RTAtom
624    specifically for storing chords, beats, etc.
625    '''
626
627    def __init__(self, src='', container=None):
628        # this stores the source
629        super().__init__(src)
630        self.container = container
631
632    def isAtom(self):
633        return True
634
635    # for lower level distinctions, use isinstance(), as each type has its own subclass.
636
637
638class RTChord(RTAtom):
639    r'''An RTAtom subclass that defines a chord.  Also contains a reference to
640    the container.
641
642    >>> chordIV = romanText.rtObjects.RTChord('IV')
643    >>> chordIV
644    <music21.romanText.rtObjects.RTChord 'IV'>
645    '''
646
647    def __init__(self, src='', container=None):
648        super().__init__(src, container)
649
650        # store offset within measure
651        self.offset = None
652        # store a quarterLength duration
653        self.quarterLength = None
654
655
656class RTNoChord(RTAtom):
657    r'''An RTAtom subclass that defines absence of a chord.  Also contains a
658    reference to the container.
659
660    >>> chordNC = romanText.rtObjects.RTNoChord('NC')
661    >>> chordNC
662    <music21.romanText.rtObjects.RTNoChord 'NC'>
663
664    >>> rth = romanText.rtObjects.RTHandler()
665    >>> rth.tokenizeAtoms('nc NC N.C.')
666    [<music21.romanText.rtObjects.RTNoChord 'nc'>,
667     <music21.romanText.rtObjects.RTNoChord 'NC'>,
668     <music21.romanText.rtObjects.RTNoChord 'N.C.'>]
669    '''
670
671    def __init__(self, src='', container=None):
672        super().__init__(src, container)
673
674        # store offset within measure
675        self.offset = None
676        # store a quarterLength duration
677        self.quarterLength = None
678
679
680class RTBeat(RTAtom):
681    r'''An RTAtom subclass that defines a beat definition.  Also contains a
682    reference to the container.
683
684    >>> beatFour = romanText.rtObjects.RTBeat('b4')
685    >>> beatFour
686    <music21.romanText.rtObjects.RTBeat 'b4'>
687    '''
688
689    def getBeatFloatOrFrac(self):
690        '''
691        Gets the beat number as a float or fraction. Time signature independent
692
693        >>> RTB = romanText.rtObjects.RTBeat
694
695        Simple ones:
696
697        >>> RTB('b1').getBeatFloatOrFrac()
698        1.0
699        >>> RTB('b2').getBeatFloatOrFrac()
700        2.0
701
702        etc.
703
704        with easy float:
705
706        >>> RTB('b1.5').getBeatFloatOrFrac()
707        1.5
708        >>> RTB('b1.25').getBeatFloatOrFrac()
709        1.25
710
711        with harder:
712
713        >>> RTB('b1.33').getBeatFloatOrFrac()
714        Fraction(4, 3)
715
716        >>> RTB('b2.66').getBeatFloatOrFrac()
717        Fraction(8, 3)
718
719        >>> RTB('b1.2').getBeatFloatOrFrac()
720        Fraction(6, 5)
721
722
723        A third digit of 0.5 adds 1/2 of 1/DENOM of before.  Here DENOM is 3 (in 5/3) so
724        we add 1/6 to 5/3 to get 11/6:
725
726
727        >>> RTB('b1.66').getBeatFloatOrFrac()
728        Fraction(5, 3)
729
730        >>> RTB('b1.66.5').getBeatFloatOrFrac()
731        Fraction(11, 6)
732
733
734        Similarly 0.25 adds 1/4 of 1/DENOM... to get 21/12 or 7/4 or 1.75
735
736        >>> RTB('b1.66.25').getBeatFloatOrFrac()
737        1.75
738
739        And 0.75 adds 3/4 of 1/DENOM to get 23/12
740
741        >>> RTB('b1.66.75').getBeatFloatOrFrac()
742        Fraction(23, 12)
743
744
745        A weird way of writing 'b1.5'
746
747        >>> RTB('b1.33.5').getBeatFloatOrFrac()
748        1.5
749        '''
750        beatStr = self.src.replace('b', '')
751        # there may be more than one decimal in the number, such as
752        # 1.66.5, to show halfway through 2/3rd of a beat
753        parts = beatStr.split('.')
754        mainBeat = int(parts[0])
755        if len(parts) > 1:  # 1.66
756            fracPart = common.addFloatPrecision('.' + parts[1])
757        else:
758            fracPart = 0.0
759
760        if len(parts) > 2:  # 1.66.5
761            fracPartDivisor = float('.' + parts[2])  # 0.5
762            if isinstance(fracPart, float):
763                fracPart = fractions.Fraction.from_float(fracPart)
764            denom = fracPart.denominator
765            fracBeatFrac = common.opFrac(1. / (denom / fracPartDivisor))
766        else:
767            fracBeatFrac = 0.0
768
769        if len(parts) > 3:
770            environLocal.printDebug([f'got unexpected beat: {self.src}'])
771            raise RTTokenException(f'cannot handle specification: {self.src}')
772
773        beat = common.opFrac(mainBeat + fracPart + fracBeatFrac)
774        return beat
775
776    def getOffset(self, timeSignature):
777        '''Given a time signature, return the offset position specified by this
778        beat.
779
780        >>> rtb = romanText.rtObjects.RTBeat('b1.5')
781        >>> rtb.getOffset(meter.TimeSignature('3/4'))
782        0.5
783        >>> rtb.getOffset(meter.TimeSignature('6/8'))
784        0.75
785        >>> rtb.getOffset(meter.TimeSignature('2/2'))
786        1.0
787
788        >>> rtb = romanText.rtObjects.RTBeat('b2')
789        >>> rtb.getOffset(meter.TimeSignature('3/4'))
790        1.0
791        >>> rtb.getOffset(meter.TimeSignature('6/8'))
792        1.5
793
794        >>> rtb = romanText.rtObjects.RTBeat('b1.66')
795        >>> rtb.getOffset(meter.TimeSignature('6/8'))
796        1.0
797        >>> rtc = romanText.rtObjects.RTBeat('b1.66.5')
798        >>> rtc.getOffset(meter.TimeSignature('6/8'))
799        1.25
800        '''
801        beat = self.getBeatFloatOrFrac()
802
803        # environLocal.printDebug(['using beat value:', beat])
804        # TODO: check for exceptions/errors if this beat is bad
805        try:
806            post = timeSignature.getOffsetFromBeat(beat)
807        except exceptions21.TimeSignatureException:
808            environLocal.printDebug(['bad beat specification: %s in a meter of %s' % (
809                                    self.src, timeSignature)])
810            post = 0.0
811
812        return post
813
814
815class RTKeyTypeAtom(RTAtom):
816    '''RTKeyTypeAtoms contain utility functions for all Key-type tokens, i.e.
817    RTKey, RTAnalyticKey, but not KeySignature.
818
819    >>> gMinor = romanText.rtObjects.RTKeyTypeAtom('g;:')
820    >>> gMinor
821    <music21.romanText.rtObjects.RTKeyTypeAtom 'g;:'>
822    '''
823
824    def getKey(self):
825        '''
826        This returns a Key, not a KeySignature object
827        '''
828        myKey = self.src.rstrip(self.footerStrip)
829        myKey = key.convertKeyStringToMusic21KeyString(myKey)
830        return key.Key(myKey)
831
832    def getKeySignature(self):
833        '''Get a KeySignature object.
834        '''
835        myKey = self.getKey()
836        return key.KeySignature(myKey.sharps)
837
838
839class RTKey(RTKeyTypeAtom):
840    '''An RTKey(RTAtom) defines both a change in KeySignature and a change
841    in the analyzed Key.
842
843    They are defined by ";:" after the Key.
844
845    >>> gMinor = romanText.rtObjects.RTKey('g;:')
846    >>> gMinor
847    <music21.romanText.rtObjects.RTKey 'g;:'>
848    >>> gMinor.getKey()
849    <music21.key.Key of g minor>
850
851    >>> bMinor = romanText.rtObjects.RTKey('bb;:')
852    >>> bMinor
853    <music21.romanText.rtObjects.RTKey 'bb;:'>
854    >>> bMinor.getKey()
855    <music21.key.Key of b- minor>
856    >>> bMinor.getKeySignature()
857    <music21.key.KeySignature of 5 flats>
858
859    >>> eFlatMajor = romanText.rtObjects.RTKey('Eb;:')
860    >>> eFlatMajor
861    <music21.romanText.rtObjects.RTKey 'Eb;:'>
862    >>> eFlatMajor.getKey()
863    <music21.key.Key of E- major>
864    '''
865    footerStrip = ';:'
866
867
868class RTAnalyticKey(RTKeyTypeAtom):
869    '''An RTAnalyticKey(RTKeyTypeAtom) only defines a change in the key
870    being analyzed.  It does not in itself create a :class:~'music21.key.Key'
871    object.
872
873    >>> gMinor = romanText.rtObjects.RTAnalyticKey('g:')
874    >>> gMinor
875    <music21.romanText.rtObjects.RTAnalyticKey 'g:'>
876    >>> gMinor.getKey()
877    <music21.key.Key of g minor>
878
879    >>> bMinor = romanText.rtObjects.RTAnalyticKey('bb:')
880    >>> bMinor
881    <music21.romanText.rtObjects.RTAnalyticKey 'bb:'>
882    >>> bMinor.getKey()
883    <music21.key.Key of b- minor>
884
885    '''
886    footerStrip = ':'
887
888
889class RTKeySignature(RTAtom):
890    '''
891    An RTKeySignature(RTAtom) only defines a change in the KeySignature.
892    It does not in itself create a :class:~'music21.key.Key' object, nor
893    does it change the analysis taking place.
894
895    The number after KS defines the number of sharps (negative for flats).
896
897    >>> gMinor = romanText.rtObjects.RTKeySignature('KS-2')
898    >>> gMinor
899    <music21.romanText.rtObjects.RTKeySignature 'KS-2'>
900    >>> gMinor.getKeySignature()
901    <music21.key.KeySignature of 2 flats>
902
903    >>> aMajor = romanText.rtObjects.RTKeySignature('KS3')
904    >>> aMajor.getKeySignature()
905    <music21.key.KeySignature of 3 sharps>
906    '''
907
908    def getKeySignature(self):
909        numSharps = int(self.src[2:])
910        return key.KeySignature(numSharps)
911
912
913class RTOpenParens(RTAtom):
914    '''
915    A simple open parenthesis Atom with a sensible default
916
917    >>> romanText.rtObjects.RTOpenParens('(')
918    <music21.romanText.rtObjects.RTOpenParens '('>
919    '''
920
921    def __init__(self, src='(', container=None):  # pylint: disable=useless-super-delegation
922        super().__init__(src, container)
923
924
925class RTCloseParens(RTAtom):
926    '''
927    A simple close parenthesis Atom with a sensible default
928
929    >>> romanText.rtObjects.RTCloseParens(')')
930    <music21.romanText.rtObjects.RTCloseParens ')'>
931    '''
932
933    def __init__(self, src=')', container=None):  # pylint: disable=useless-super-delegation
934        super().__init__(src, container)
935
936
937class RTOptionalKeyOpen(RTAtom):
938    '''
939    Marks the beginning of an optional Key area which does not
940    affect the roman numeral analysis.  (For instance, it is
941    possible to analyze in Bb major, while remaining in g minor)
942
943    >>> possibleKey = romanText.rtObjects.RTOptionalKeyOpen('?(Bb:')
944    >>> possibleKey
945    <music21.romanText.rtObjects.RTOptionalKeyOpen '?(Bb:'>
946    >>> possibleKey.getKey()
947    <music21.key.Key of B- major>
948    '''
949
950    def getKey(self):
951        # alter flat symbol
952        if self.src == '?(b:':
953            return key.Key('b')
954        else:
955            keyStr = self.src.replace('b', '-')
956            keyStr = keyStr.replace(':', '')
957            keyStr = keyStr.replace('?', '')
958            keyStr = keyStr.replace('(', '')
959            # environLocal.printDebug(['create a key from:', keyStr])
960            return key.Key(keyStr)
961
962
963class RTOptionalKeyClose(RTAtom):
964    '''
965    Marks the end of an optional Key area which does not affect the roman
966    numeral analysis.
967
968    For example, it is possible to analyze in Bb major, while remaining in g
969    minor.
970
971    >>> possibleKey = romanText.rtObjects.RTOptionalKeyClose('?)Bb:')
972    >>> possibleKey
973    <music21.romanText.rtObjects.RTOptionalKeyClose '?)Bb:'>
974    >>> possibleKey.getKey()
975    <music21.key.Key of B- major>
976    '''
977
978    def getKey(self):
979        # alter flat symbol
980        if self.src == '?)b:' or self.src == '?)b':
981            return key.Key('b')
982        else:
983            keyStr = self.src.replace('b', '-')
984            keyStr = keyStr.replace(':', '')
985            keyStr = keyStr.replace('?', '')
986            keyStr = keyStr.replace(')', '')
987            # environLocal.printDebug(['create a key from:', keyStr])
988            return key.Key(keyStr)
989
990
991class RTPhraseMarker(RTAtom):
992    '''
993    A Phrase Marker:
994
995    >>> rtPhraseMarker = romanText.rtObjects.RTPhraseMarker('')
996    >>> rtPhraseMarker
997    <music21.romanText.rtObjects.RTPhraseMarker ''>
998    '''
999
1000
1001class RTPhraseBoundary(RTPhraseMarker):
1002    '''
1003    >>> phrase = romanText.rtObjects.RTPhraseBoundary('||')
1004    >>> phrase
1005    <music21.romanText.rtObjects.RTPhraseBoundary '||'>
1006    '''
1007
1008    def __init__(self, src='||', container=None):  # pylint: disable=useless-super-delegation
1009        super().__init__(src, container)
1010
1011
1012class RTEllisonStart(RTPhraseMarker):
1013    '''
1014    >>> phrase = romanText.rtObjects.RTEllisonStart('|*')
1015    >>> phrase
1016    <music21.romanText.rtObjects.RTEllisonStart '|*'>
1017    '''
1018
1019    def __init__(self, src='|*', container=None):  # pylint: disable=useless-super-delegation
1020        super().__init__(src, container)
1021
1022
1023class RTEllisonStop(RTPhraseMarker):
1024    '''
1025    >>> phrase = romanText.rtObjects.RTEllisonStop('*|')
1026    >>> phrase
1027    <music21.romanText.rtObjects.RTEllisonStop '*|'>
1028    '''
1029
1030    def __init__(self, src='*|', container=None):  # pylint: disable=useless-super-delegation
1031        super().__init__(src, container)
1032
1033
1034class RTRepeat(RTAtom):
1035    '''
1036    >>> repeat = romanText.rtObjects.RTRepeat('||:')
1037    >>> repeat
1038    <music21.romanText.rtObjects.RTRepeat '||:'>
1039    '''
1040
1041
1042class RTRepeatStart(RTRepeat):
1043    '''
1044    >>> repeat = romanText.rtObjects.RTRepeatStart()
1045    >>> repeat
1046    <music21.romanText.rtObjects.RTRepeatStart ...'||:'>
1047    '''
1048
1049    def __init__(self, src='||:', container=None):  # pylint: disable=useless-super-delegation
1050        super().__init__(src, container)
1051
1052
1053class RTRepeatStop(RTRepeat):
1054    '''
1055    >>> repeat = romanText.rtObjects.RTRepeatStop()
1056    >>> repeat
1057    <music21.romanText.rtObjects.RTRepeatStop ...':||'>
1058    '''
1059
1060    def __init__(self, src=':||', container=None):  # pylint: disable=useless-super-delegation
1061        super().__init__(src, container)
1062
1063
1064# ------------------------------------------------------------------------------
1065
1066class RTHandler:
1067
1068    # divide elements of a character stream into rtObjects and handle
1069    # store in a list, and pass global information to components
1070    def __init__(self):
1071        # tokens are ABC rtObjects in a linear stream
1072        # tokens are strongly divided between header and body, so can
1073        # divide here
1074        self._tokens = []
1075        self.currentLineNumber = 0
1076
1077    def splitAtHeader(self, lines):
1078        '''Divide string into header and non-header; this is done before
1079        tokenization.
1080
1081        >>> rth = romanText.rtObjects.RTHandler()
1082        >>> rth.splitAtHeader(['Title: s', 'Time Signature:', '', 'm1 g: i'])
1083        (['Title: s', 'Time Signature:', ''], ['m1 g: i'])
1084
1085        '''
1086        # iterate over lines and find the first measure definition
1087        iStartBody = None
1088        for i, l in enumerate(lines):
1089            if reMeasureTag.match(l.strip()) is not None:
1090                # found a measure definition
1091                iStartBody = i
1092                break
1093        if iStartBody is None:
1094            raise RomanTextException('Cannot find the first measure definition in this file. '
1095                                     + 'Dumping contexts: %s', lines)
1096        return lines[:iStartBody], lines[iStartBody:]
1097
1098    def tokenizeHeader(self, lines):
1099        '''In the header, we only have :class:`~music21.romanText.base.RTTagged`
1100        tokens. We can this process these all as the same class.
1101        '''
1102        post = []
1103        for i, line in enumerate(lines):
1104            line = line.strip()
1105            if line == '':
1106                continue
1107            # wrap each line in a header token
1108            rtt = RTTagged(line)
1109            rtt.lineNumber = i + 1
1110            post.append(rtt)
1111        self.currentLineNumber = len(lines) + 1
1112        return post
1113
1114    def tokenizeBody(self, lines):
1115        '''In the body, we may have measure, time signature, or note
1116        declarations, as well as possible other tagged definitions.
1117        '''
1118        post = []
1119        startLineNumber = self.currentLineNumber
1120        for i, line in enumerate(lines):
1121            currentLineNumber = startLineNumber + i
1122            try:
1123                line = line.strip()
1124                if line == '':
1125                    continue
1126                # first, see if it is a measure definition, if not, than assume it is tagged data
1127                if reMeasureTag.match(line) is not None:
1128                    rtm = RTMeasure(line)
1129                    rtm.lineNumber = currentLineNumber
1130                    # note: could places these in-line, after post
1131                    rtm.atoms = self.tokenizeAtoms(rtm.data, container=rtm)
1132                    for a in rtm.atoms:
1133                        a.lineNumber = currentLineNumber
1134                    post.append(rtm)
1135                else:
1136                    # store items in a measure tag outside of the measure
1137                    rtt = RTTagged(line)
1138                    rtt.lineNumber = currentLineNumber
1139                    post.append(rtt)
1140            except Exception:
1141                import traceback
1142                tracebackMessage = traceback.format_exc()
1143                raise RTHandlerException('At line %s (%s) an exception was raised: \n%s' % (
1144                    currentLineNumber, line, tracebackMessage))
1145        return post
1146
1147    def tokenizeAtoms(self, line, container=None):
1148        '''Given a line of data stored in measure consisting only of Atoms,
1149        tokenize and return a list.
1150
1151        >>> rth = romanText.rtObjects.RTHandler()
1152        >>> rth.tokenizeAtoms('IV b3 ii7 b4 ii')
1153        [<music21.romanText.rtObjects.RTChord 'IV'>,
1154         <music21.romanText.rtObjects.RTBeat 'b3'>,
1155         <music21.romanText.rtObjects.RTChord 'ii7'>,
1156         <music21.romanText.rtObjects.RTBeat 'b4'>,
1157         <music21.romanText.rtObjects.RTChord 'ii'>]
1158
1159        >>> rth.tokenizeAtoms('V7 b2 V13 b3 V7 iio6/5[no5]')
1160        [<music21.romanText.rtObjects.RTChord 'V7'>,
1161         <music21.romanText.rtObjects.RTBeat 'b2'>,
1162         <music21.romanText.rtObjects.RTChord 'V13'>,
1163         <music21.romanText.rtObjects.RTBeat 'b3'>,
1164         <music21.romanText.rtObjects.RTChord 'V7'>,
1165         <music21.romanText.rtObjects.RTChord 'iio6/5[no5]'>]
1166
1167        >>> tokenList = rth.tokenizeAtoms('I b2 I b2.25 V/ii b2.5 bVII b2.75 V g: IV')
1168        >>> tokenList
1169        [<music21.romanText.rtObjects.RTChord 'I'>,
1170         <music21.romanText.rtObjects.RTBeat 'b2'>,
1171         <music21.romanText.rtObjects.RTChord 'I'>,
1172         <music21.romanText.rtObjects.RTBeat 'b2.25'>,
1173         <music21.romanText.rtObjects.RTChord 'V/ii'>,
1174         <music21.romanText.rtObjects.RTBeat 'b2.5'>,
1175         <music21.romanText.rtObjects.RTChord 'bVII'>,
1176         <music21.romanText.rtObjects.RTBeat 'b2.75'>,
1177         <music21.romanText.rtObjects.RTChord 'V'>,
1178         <music21.romanText.rtObjects.RTAnalyticKey 'g:'>,
1179         <music21.romanText.rtObjects.RTChord 'IV'>]
1180
1181        >>> tokenList[-2].getKey()
1182        <music21.key.Key of g minor>
1183
1184        >>> rth.tokenizeAtoms('= m3')
1185        []
1186
1187        >>> tokenList = rth.tokenizeAtoms('g;: ||: V b2 ?(Bb: VII7 b3 III b4 ?)Bb: i :||')
1188        >>> tokenList
1189        [<music21.romanText.rtObjects.RTKey 'g;:'>,
1190         <music21.romanText.rtObjects.RTRepeatStart '||:'>,
1191         <music21.romanText.rtObjects.RTChord 'V'>,
1192         <music21.romanText.rtObjects.RTBeat 'b2'>,
1193         <music21.romanText.rtObjects.RTOptionalKeyOpen '?(Bb:'>,
1194         <music21.romanText.rtObjects.RTChord 'VII7'>,
1195         <music21.romanText.rtObjects.RTBeat 'b3'>,
1196         <music21.romanText.rtObjects.RTChord 'III'>,
1197         <music21.romanText.rtObjects.RTBeat 'b4'>,
1198         <music21.romanText.rtObjects.RTOptionalKeyClose '?)Bb:'>,
1199         <music21.romanText.rtObjects.RTChord 'i'>,
1200         <music21.romanText.rtObjects.RTRepeatStop ':||'>]
1201        '''
1202        post = []
1203        # break by spaces
1204        for word in line.split(' '):
1205            word = word.strip()
1206            if word == '':
1207                continue
1208            elif word == '=':
1209                # if an = is found, this is a copy definition, and no atoms here
1210                break
1211            elif word == '||':
1212                post.append(RTPhraseBoundary(word, container))
1213            elif word == '(':
1214                post.append(RTOpenParens(word, container))
1215            elif word == ')':
1216                post.append(RTCloseParens(word, container))
1217            elif reBeatAtom.match(word) is not None:
1218                post.append(RTBeat(word, container))
1219            # from here, all that is left is keys or chords
1220            elif reOptKeyOpenAtom.match(word) is not None:
1221                post.append(RTOptionalKeyOpen(word, container))
1222            elif reOptKeyCloseAtom.match(word) is not None:
1223                post.append(RTOptionalKeyClose(word, container))
1224            elif reKeyAtom.match(word) is not None:
1225                post.append(RTKey(word, container))
1226            elif reAnalyticKeyAtom.match(word) is not None:
1227                post.append(RTAnalyticKey(word, container))
1228            elif reKeySignatureAtom.match(word) is not None:
1229                post.append(RTKeySignature(word, container))
1230            elif reRepeatStartAtom.match(word) is not None:
1231                post.append(RTRepeatStart(word, container))
1232            elif reRepeatStopAtom.match(word) is not None:
1233                post.append(RTRepeatStop(word, container))
1234            elif reNoChordAtom.match(word) is not None:
1235                post.append(RTNoChord(word, container))
1236            else:  # only option is that it is a chord
1237                post.append(RTChord(word, container))
1238        return post
1239
1240    def tokenize(self, src):
1241        '''
1242        Walk the RT string, creating RT rtObjects along the way.
1243        '''
1244        # break into lines
1245        lines = src.split('\n')
1246        linesHeader, linesBody = self.splitAtHeader(lines)
1247        # environLocal.printDebug([linesHeader])
1248        self._tokens += self.tokenizeHeader(linesHeader)
1249        self._tokens += self.tokenizeBody(linesBody)
1250
1251    def process(self, src):
1252        '''
1253        Given an entire specification as a single source string, strSrc, tokenize it.
1254        This is usually provided in a file.
1255        '''
1256        self._tokens = []
1257        self.tokenize(src)
1258
1259    def definesMovements(self, countRequired=2):
1260        '''Return True if more than one movement is defined in a RT file.
1261
1262        >>> rth = romanText.rtObjects.RTHandler()
1263        >>> rth.process('Movement: 1 \\n Movement: 2 \\n \\n m1')
1264        >>> rth.definesMovements()
1265        True
1266        >>> rth.process('Movement: 1 \\n m1')
1267        >>> rth.definesMovements()
1268        False
1269        '''
1270        if not self._tokens:
1271            raise RTHandlerException('must create tokens first')
1272        count = 0
1273        for t in self._tokens:
1274            if t.isMovement():
1275                count += 1
1276                if count >= countRequired:
1277                    return True
1278        return False
1279
1280    def definesMovement(self):
1281        '''Return True if this handler has 1 or more movement.
1282
1283        >>> rth = romanText.rtObjects.RTHandler()
1284        >>> rth.process('Movement: 1 \\n \\n m1')
1285        >>> rth.definesMovements()
1286        False
1287        >>> rth.definesMovement()
1288        True
1289        '''
1290        return self.definesMovements(countRequired=1)
1291
1292    def splitByMovement(self, duplicateHeader=True):
1293        '''If we have movements defined, return a list of RTHandler rtObjects,
1294        representing header information and each movement, in order.
1295
1296        >>> rth = romanText.rtObjects.RTHandler()
1297        >>> rth.process('Title: Test \\n Movement: 1 \\n m1 \\n Movement: 2 \\n m1')
1298        >>> post = rth.splitByMovement(False)
1299        >>> len(post)
1300        3
1301
1302        >>> len(post[0])
1303        1
1304
1305        >>> post[0].__class__
1306        <class 'music21.romanText.rtObjects.RTHandler'>
1307        >>> len(post[1]), len(post[2])
1308        (2, 2)
1309
1310        >>> post = rth.splitByMovement(duplicateHeader=True)
1311        >>> len(post)
1312        2
1313
1314        >>> len(post[0]), len(post[1])
1315        (3, 3)
1316        '''
1317        post = []
1318        sub = []
1319        for t in self._tokens:
1320            if t.isMovement():
1321                # when finding a movement, we are ending a previous
1322                # and starting a new; this may just have metadata
1323                rth = RTHandler()
1324                rth.tokens = sub
1325                post.append(rth)
1326                sub = []
1327            sub.append(t)
1328
1329        if sub:
1330            rth = RTHandler()
1331            rth.tokens = sub
1332            post.append(rth)
1333
1334        if duplicateHeader:
1335            alt = []
1336            # if no movement in this first handler, assume it is header info
1337            if not post[0].definesMovement():
1338                handlerHead = post[0]
1339                iStart = 1
1340            else:
1341                handlerHead = None
1342                iStart = 0
1343            for h in post[iStart:]:
1344                if handlerHead is not None:
1345                    h = handlerHead + h  # add metadata
1346                alt.append(h)
1347            # reassign
1348            post = alt
1349
1350        return post
1351
1352    # --------------------------------------------------------------------------
1353    # access tokens
1354
1355    def _getTokens(self):
1356        if not self._tokens:
1357            raise RTHandlerException('must process tokens before calling split')
1358        return self._tokens
1359
1360    def _setTokens(self, tokens):
1361        '''Assign tokens to this Handler.
1362        '''
1363        self._tokens = tokens
1364
1365    tokens = property(_getTokens, _setTokens,
1366                      doc='''Get or set tokens for this Handler.
1367        ''')
1368
1369    def __len__(self):
1370        return len(self._tokens)
1371
1372    def __add__(self, other):
1373        '''Return a new handler adding the tokens in both
1374        '''
1375        rth = self.__class__()  # will get the same class type
1376        rth.tokens = self._tokens + other._tokens
1377        return rth
1378
1379
1380# ------------------------------------------------------------------------------
1381
1382class RTFile(prebase.ProtoM21Object):
1383    '''
1384    Roman Text File access.
1385    '''
1386
1387    def __init__(self):
1388        self.file = None
1389        self.filename = None
1390
1391    def open(self, filename):
1392        '''Open a file for reading, trying a variety of encodings and then
1393        trying them again with an ignore if it is not possible.
1394        '''
1395        for encoding in ('utf-8', 'macintosh', 'latin-1', 'utf-16'):
1396            try:
1397                # pylint: disable=consider-using-with
1398                self.file = io.open(filename, encoding=encoding)
1399                if self.file is not None:
1400                    break
1401            except UnicodeDecodeError:
1402                pass
1403        if self.file is None:
1404            for encoding in ('utf-8', 'macintosh', 'latin-1', 'utf-16', None):
1405                try:
1406                    # pylint: disable=consider-using-with
1407                    self.file = io.open(filename, encoding=encoding, errors='ignore')
1408                    if self.file is not None:
1409                        break
1410                except UnicodeDecodeError:
1411                    pass
1412            if self.file is None:
1413                raise RomanTextException(
1414                    f'Cannot parse file {filename}, possibly a broken codec?')
1415
1416        self.filename = filename
1417
1418    def openFileLike(self, fileLike):
1419        '''Assign a file-like object, such as those provided by StringIO, as an
1420        open file object.
1421        '''
1422        self.file = fileLike  # already 'open'
1423
1424    def _reprInternal(self):
1425        return ''
1426
1427    def close(self):
1428        self.file.close()
1429
1430    def read(self):
1431        '''Read a file. Note that this calls readstr, which processes all tokens.
1432
1433        If `number` is given, a work number will be extracted if possible.
1434        '''
1435        return self.readstr(self.file.read())
1436
1437    def readstr(self, strSrc):
1438        '''Read a string and process all Tokens. Returns a ABCHandler instance.
1439        '''
1440        handler = RTHandler()
1441        # return the handler instance
1442        handler.process(strSrc)
1443        return handler
1444
1445
1446# ------------------------------------------------------------------------------
1447
1448class Test(unittest.TestCase):
1449
1450    def testBasicA(self):
1451        from music21.romanText import testFiles
1452        for fileStr in testFiles.ALL:
1453            f = RTFile()
1454            unused_rth = f.readstr(fileStr)  # get a handler from a string
1455
1456    def testReA(self):
1457        # gets the index of the end of the measure indication
1458        g = reMeasureTag.match('m1 g: V b2 i')
1459        self.assertEqual(g.end(), 2)
1460        self.assertEqual(g.group(0), 'm1')
1461
1462        self.assertEqual(reMeasureTag.match('Time Signature: 2/2'), None)
1463
1464        g = reMeasureTag.match('m3-4=m1-2')
1465        self.assertEqual(g.end(), 4)
1466        self.assertEqual(g.start(), 0)
1467        self.assertEqual(g.group(0), 'm3-4')
1468
1469        g = reMeasureTag.match('m123-432=m1120-24234')
1470        self.assertEqual(g.group(0), 'm123-432')
1471
1472        g = reMeasureTag.match('m231a IV6 b4 C: V')
1473        self.assertEqual(g.group(0), 'm231a')
1474
1475        g = reMeasureTag.match('m123b-432b=m1120a-24234a')
1476        self.assertEqual(g.group(0), 'm123b-432b')
1477
1478        g = reMeasureTag.match('m231var1 IV6 b4 C: V')
1479        self.assertEqual(g.group(0), 'm231')
1480
1481        # this only works if it starts the string
1482        g = reVariant.match('var1 IV6 b4 C: V')
1483        self.assertEqual(g.group(0), 'var1')
1484
1485        g = reAnalyticKeyAtom.match('Bb:')
1486        self.assertEqual(g.group(0), 'Bb:')
1487        g = reAnalyticKeyAtom.match('F#:')
1488        self.assertEqual(g.group(0), 'F#:')
1489        g = reAnalyticKeyAtom.match('f#:')
1490        self.assertEqual(g.group(0), 'f#:')
1491        g = reAnalyticKeyAtom.match('b:')
1492        self.assertEqual(g.group(0), 'b:')
1493        g = reAnalyticKeyAtom.match('bb:')
1494        self.assertEqual(g.group(0), 'bb:')
1495        g = reAnalyticKeyAtom.match('g:')
1496        self.assertEqual(g.group(0), 'g:')
1497
1498        # beats do not have a colon
1499        self.assertEqual(reKeyAtom.match('b2'), None)
1500        self.assertEqual(reKeyAtom.match('b2.5'), None)
1501
1502        g = reBeatAtom.match('b2.5')
1503        self.assertEqual(g.group(0), 'b2.5')
1504
1505        g = reBeatAtom.match('bVII')
1506        self.assertEqual(g, None)
1507
1508        g = reBeatAtom.match('b1.66.5')
1509        self.assertEqual(g.group(0), 'b1.66.5')
1510
1511    def testMeasureAttributeProcessing(self):
1512        rtm = RTMeasure('m17var1 vi b2 IV b2.5 viio6/4 b3.5 I')
1513        self.assertEqual(rtm.data, 'vi b2 IV b2.5 viio6/4 b3.5 I')
1514        self.assertEqual(rtm.number, [17])
1515        self.assertEqual(rtm.tag, 'm17')
1516        self.assertEqual(rtm.variantNumber, 1)
1517
1518        rtm = RTMeasure('m17varC vi b2 IV b2.5 viio6/4 b3.5 I')
1519        self.assertEqual(rtm.data, 'vi b2 IV b2.5 viio6/4 b3.5 I')
1520        self.assertEqual(rtm.variantLetter, 'C')
1521
1522        rtm = RTMeasure('m20 vi b2 ii6/5 b3 V b3.5 V7')
1523        self.assertEqual(rtm.data, 'vi b2 ii6/5 b3 V b3.5 V7')
1524        self.assertEqual(rtm.number, [20])
1525        self.assertEqual(rtm.tag, 'm20')
1526        self.assertEqual(rtm.variantNumber, None)
1527        self.assertFalse(rtm.isCopyDefinition)
1528
1529        rtm = RTMeasure('m0 b3 G: I')
1530        self.assertEqual(rtm.data, 'b3 G: I')
1531        self.assertEqual(rtm.number, [0])
1532        self.assertEqual(rtm.tag, 'm0')
1533        self.assertEqual(rtm.variantNumber, None)
1534        self.assertFalse(rtm.isCopyDefinition)
1535
1536        rtm = RTMeasure('m59 = m57')
1537        self.assertEqual(rtm.data, '= m57')
1538        self.assertEqual(rtm.number, [59])
1539        self.assertEqual(rtm.tag, 'm59')
1540        self.assertEqual(rtm.variantNumber, None)
1541        self.assertTrue(rtm.isCopyDefinition)
1542
1543        rtm = RTMeasure('m3-4 = m1-2')
1544        self.assertEqual(rtm.data, '= m1-2')
1545        self.assertEqual(rtm.number, [3, 4])
1546        self.assertEqual(rtm.tag, 'm3-4')
1547        self.assertEqual(rtm.variantNumber, None)
1548        self.assertTrue(rtm.isCopyDefinition)
1549
1550    def testTokenDefinition(self):
1551        # test that we are always getting the right number of tokens
1552        from music21.romanText import testFiles
1553
1554        rth = RTHandler()
1555        rth.process(testFiles.mozartK279)
1556
1557        count = 0
1558        for t in rth._tokens:
1559            if t.isMovement():
1560                count += 1
1561        self.assertEqual(count, 3)
1562
1563        rth.process(testFiles.riemenschneider001)
1564        count = 0
1565        for t in rth._tokens:
1566            if t.isMeasure():
1567                # print(t.src)
1568                count += 1
1569        # 21, 2 variants, and one pickup
1570        self.assertEqual(count, 21 + 2 + 1)
1571
1572        count = 0
1573        for t in rth._tokens:
1574            if t.isMeasure():
1575                for a in t.atoms:
1576                    if isinstance(a, RTAnalyticKey):
1577                        count += 1
1578        self.assertEqual(count, 1)
1579
1580
1581# ------------------------------------------------------------------------------
1582# define presented order in documentation
1583# _DOC_ORDER = []
1584
1585if __name__ == '__main__':
1586    import music21
1587    music21.mainTest(Test)
1588
1589