1# -*- coding: utf-8 -*-
2# ------------------------------------------------------------------------------
3# Name:         bar.py
4# Purpose:      music21 classes for representing bars, repeats, and related
5#
6# Authors:      Michael Scott Cuthbert
7#               Christopher Ariza
8#
9# Copyright:    Copyright © 2009-2012, 2020 Michael Scott Cuthbert and the music21 Project
10# License:      BSD, see license.txt
11# ------------------------------------------------------------------------------
12'''
13Object models of barlines, including repeat barlines.
14'''
15import unittest
16from typing import Optional
17
18from music21 import base
19from music21 import exceptions21
20
21from music21 import expressions
22from music21 import repeat
23
24from music21 import environment
25
26_MOD = 'bar'
27environLocal = environment.Environment(_MOD)
28
29# ------------------------------------------------------------------------------
30
31class BarException(exceptions21.Music21Exception):
32    pass
33
34
35# store alternative names for types; use this dictionary for translation
36# reference
37barTypeList = [
38    'regular', 'dotted', 'dashed', 'heavy', 'double', 'final',
39    'heavy-light', 'heavy-heavy', 'tick', 'short', 'none',
40]
41
42# former are MusicXML names we allow
43barTypeDict = {
44    'light-light': 'double',
45    'light-heavy': 'final'
46}
47reverseBarTypeDict = {
48    'double': 'light-light',
49    'final': 'light-heavy',
50}
51
52
53def typeToMusicXMLBarStyle(value):
54    '''
55    Convert a music21 barline name into the musicxml name --
56    essentially just changes the names of 'double' and 'final'
57    to 'light-light' and 'light-heavy'
58
59    Does not do error checking to make sure it's a valid name,
60    since setting the style on a Barline object already does that.
61
62    >>> bar.typeToMusicXMLBarStyle('final')
63    'light-heavy'
64    >>> bar.typeToMusicXMLBarStyle('regular')
65    'regular'
66    '''
67    if value.lower() in reverseBarTypeDict:
68        return reverseBarTypeDict[value.lower()]
69    else:
70        return value
71
72def standardizeBarType(value):
73    '''
74    Standardizes bar type names.
75
76    converts all names to lower case, None to 'regular',
77    and 'light-light' to 'double' and 'light-heavy' to 'final',
78    raises an error for unknown styles.
79    '''
80    if value is None:
81        return 'regular'  # for now, return with string
82
83    value = value.lower()
84
85    if value in barTypeList:
86        return value
87    elif value in barTypeDict:
88        return barTypeDict[value]
89    # if not match
90    else:
91        raise BarException(f'cannot process style: {value}')
92
93
94# ------------------------------------------------------------------------------
95class Barline(base.Music21Object):
96    '''A representation of a barline.
97    Barlines are conventionally assigned to Measure objects
98    using the leftBarline and rightBarline attributes.
99
100
101    >>> bl = bar.Barline('double')
102    >>> bl
103    <music21.bar.Barline type=double>
104
105    The type can also just be set via a keyword of "type".  Or if no type is specified,
106    a regular barline is returned.  Location can also be explicitly stored, but it's not
107    needed except for musicxml translation:
108
109    >>> bl2 = bar.Barline(type='dashed')
110    >>> bl2
111    <music21.bar.Barline type=dashed>
112    >>> bl3 = bar.Barline()
113    >>> bl3
114    <music21.bar.Barline type=regular>
115    >>> bl4 = bar.Barline(type='final', location='right')
116    >>> bl4
117    <music21.bar.Barline type=final>
118    >>> bl4.type
119    'final'
120
121    Note that the barline type 'ticked' only is displayed correctly in Finale and Finale Notepad.
122
123    N.B. for backwards compatibility reasons, currently
124    Bar objects do not use the style.Style class since
125    the phrase "style" was already used.
126    '''
127    validStyles = list(barTypeDict.keys())
128
129    classSortOrder = -5
130
131    def __init__(self,
132                 type=None,  # @ReservedAssignment  # pylint: disable=redefined-builtin
133                 location=None):
134        super().__init__()
135
136        self._type = None  # same as style...
137        # this will raise an exception on error from property
138        self.type = type
139
140        # pause can be music21.expressions.Fermata object
141        self.pause = None
142
143        # location is primarily stored in the stream as leftBarline or rightBarline
144        # but can also be stored here.
145        self.location = location  # musicxml values: can be left, right, middle, None
146
147    def _reprInternal(self):
148        return f'type={self.type}'
149
150
151    def _getType(self):
152        return self._type
153
154    def _setType(self, value):
155        self._type = standardizeBarType(value)
156
157    type = property(_getType, _setType,
158        doc='''
159        Get and set the Barline type property.
160
161        >>> b = bar.Barline()
162        >>> b.type = 'tick'
163        >>> b.type
164        'tick'
165
166        Synonyms are given for some types, based on
167        musicxml styles:
168
169        >>> b.type = 'light-light'
170        >>> b.type
171        'double'
172        ''')
173
174    def musicXMLBarStyle(self):
175        '''
176        returns the musicxml style for the bar.  most are the same as
177        `.type` but "double" and "final" are different.
178
179        >>> b = bar.Barline('tick')
180        >>> b.musicXMLBarStyle()
181        'tick'
182
183        >>> b.type = 'double'
184        >>> b.musicXMLBarStyle()
185        'light-light'
186
187        >>> b.type = 'final'
188        >>> b.musicXMLBarStyle()
189        'light-heavy'
190
191        Changed in v.5.7 -- was a property before.
192        '''
193        return typeToMusicXMLBarStyle(self.type)
194
195
196
197
198
199
200# ------------------------------------------------------------------------------
201
202# note that musicxml permits the barline to have attributes for segno and coda
203# <xs:attribute name="segno" type="xs:token"/>
204# <xs:attribute name="coda" type="xs:token"/>
205
206# type <ending> in musicxml is used to mark different endings
207
208
209class Repeat(repeat.RepeatMark, Barline):
210    '''
211    A Repeat barline.
212
213    The `direction` parameter can be one of `start` or `end`.
214    An `end` followed by a `start`
215    should be encoded as two `bar.Repeat` signs.
216
217
218    >>> rep = bar.Repeat(direction='end', times=3)
219    >>> rep
220    <music21.bar.Repeat direction=end times=3>
221
222    To apply a repeat barline assign it to either the `.leftBarline` or
223    `.rightBarline` attribute
224    of a measure.
225
226    >>> m = stream.Measure()
227    >>> m.leftBarline = bar.Repeat(direction='start')
228    >>> m.rightBarline = bar.Repeat(direction='end')
229    >>> m.insert(0.0, meter.TimeSignature('4/4'))
230    >>> m.repeatAppend(note.Note('D--5'), 4)
231    >>> p = stream.Part()
232    >>> p.insert(0.0, m)
233    >>> p.show('text')
234    {0.0} <music21.stream.Measure 0 offset=0.0>
235        {0.0} <music21.bar.Repeat direction=start>
236        {0.0} <music21.meter.TimeSignature 4/4>
237        {0.0} <music21.note.Note D-->
238        {1.0} <music21.note.Note D-->
239        {2.0} <music21.note.Note D-->
240        {3.0} <music21.note.Note D-->
241        {4.0} <music21.bar.Repeat direction=end>
242
243    The method :meth:`~music21.stream.Part.expandRepeats` on a
244    :class:`~music21.stream.Part` object expands the repeats, but
245    does not update measure numbers
246
247    >>> q = p.expandRepeats()
248    >>> q.show('text')
249    {0.0} <music21.stream.Measure 0 offset=0.0>
250        {0.0} <music21.bar.Barline type=double>
251        {0.0} <music21.meter.TimeSignature 4/4>
252        {0.0} <music21.note.Note D-->
253        {1.0} <music21.note.Note D-->
254        {2.0} <music21.note.Note D-->
255        {3.0} <music21.note.Note D-->
256        {4.0} <music21.bar.Barline type=double>
257    {4.0} <music21.stream.Measure 0a offset=4.0>
258        {0.0} <music21.bar.Barline type=double>
259        {0.0} <music21.meter.TimeSignature 4/4>
260        {0.0} <music21.note.Note D-->
261        {1.0} <music21.note.Note D-->
262        {2.0} <music21.note.Note D-->
263        {3.0} <music21.note.Note D-->
264        {4.0} <music21.bar.Barline type=double>
265    '''
266    # _repeatDots = None  # not sure what this is for; inherited from old modules
267    def __init__(self, direction='start', times=None):
268        repeat.RepeatMark.__init__(self)
269        if direction == 'start':
270            barType = 'heavy-light'
271        else:
272            barType = 'final'
273        Barline.__init__(self, type=barType)
274
275        self._direction: Optional[str] = None  # either start or end
276        self._times: Optional[int] = None  # if an end, how many repeats
277
278        # start is forward, end is backward in musicxml
279        self.direction = direction  # start, end
280        self.times = times
281
282    def _reprInternal(self):
283        msg = f'direction={self.direction}'
284        if self.times is not None:
285            msg += f' times={self.times}'
286        return msg
287
288
289    @property
290    def direction(self) -> str:
291        '''
292        Get or set the direction of this Repeat barline. Can be start or end.
293
294        TODO: show how changing direction changes type.
295        '''
296        return self._direction
297
298    @direction.setter
299    def direction(self, value: str):
300        if value.lower() in ('start', 'end'):
301            self._direction = value.lower()
302            if self._direction == 'end':
303                self.type = 'final'
304            elif self._direction == 'start':
305                self.type = 'heavy-light'
306        else:
307            raise BarException(f'cannot set repeat direction to: {value}')
308
309    @property
310    def times(self) -> Optional[int]:
311        '''
312        Get or set the times property of this barline. This
313        defines how many times the repeat happens. A standard repeat
314        repeats 2 times; values equal to or greater than 0 are permitted.
315        A repeat of 0 skips the repeated passage.
316
317        >>> lb = bar.Repeat(direction='start')
318        >>> rb = bar.Repeat(direction='end')
319
320        Only end expressions can have times:
321
322        >>> lb.times = 3
323        Traceback (most recent call last):
324        music21.bar.BarException: cannot set repeat times on a start Repeat
325
326        >>> rb.times = 3
327        >>> rb.times = -3
328        Traceback (most recent call last):
329        music21.bar.BarException: cannot set repeat times to a value less than zero: -3
330        '''
331        return self._times
332
333    @times.setter
334    def times(self, value: int):
335        if value is None:
336            self._times = None
337        else:
338            try:
339                candidate = int(value)
340            except ValueError:
341                # pylint: disable:raise-missing-from
342                raise BarException(
343                    f'cannot set repeat times to: {value!r}'
344                )
345
346            if candidate < 0:
347                raise BarException(
348                    f'cannot set repeat times to a value less than zero: {value}'
349                )
350            if self.direction == 'start':
351                raise BarException('cannot set repeat times on a start Repeat')
352
353            self._times = candidate
354
355
356    def getTextExpression(self, prefix='', postfix='x'):
357        '''
358        Return a configured :class:`~music21.expressions.TextExpressions`
359        object describing the repeat times. Append this to the stream
360        for annotation of repeat times.
361
362        >>> rb = bar.Repeat(direction='end')
363        >>> rb.times = 3
364        >>> rb.getTextExpression()
365        <music21.expressions.TextExpression '3x'>
366
367        >>> rb.getTextExpression(prefix='repeat ', postfix=' times')
368        <music21.expressions.TextExpression 'repeat 3 t...'>
369        '''
370        value = f'{prefix}{self._times}{postfix}'
371        return expressions.TextExpression(value)
372
373
374# ------------------------------------------------------------------------------
375class Test(unittest.TestCase):
376
377    def testSortOrder(self):
378        from music21 import stream
379        from music21 import clef
380        from music21 import note
381        from music21 import metadata
382        m = stream.Measure()
383        b = Repeat()
384        m.leftBarline = b
385        c = clef.BassClef()
386        m.append(c)
387        n = note.Note()
388        m.append(n)
389
390        # check sort order
391        self.assertEqual(m[0], b)
392        self.assertEqual(m[1], c)
393        self.assertEqual(m[2], n)
394
395        # if we add metadata, it sorts ahead of bar
396        md = metadata.Metadata()
397        m.insert(0, md)
398
399        self.assertEqual(m[0], md)
400        self.assertEqual(m[1], b)
401
402    def testFreezeThaw(self):
403        from music21 import converter
404        from music21 import stream
405
406        b = Barline()
407        self.assertNotIn('StyleMixin', b.classes)
408        s = stream.Stream([b])
409        data = converter.freezeStr(s, fmt='pickle')
410        s2 = converter.thawStr(data)
411        thawedBarline = s2[0]
412        # Previously, raised AttributeError
413        self.assertEqual(thawedBarline.hasStyleInformation, False)
414
415
416if __name__ == '__main__':
417    import music21
418    music21.mainTest(Test)
419
420