Major refactoring

Added more items to shorter repeat syntax: 124:3 A=(1 2) A:4

Prefixes now work for more items: q..r qs(1 2 3)

Duration support for list operations: (q1 e3)+(1 4 3 5)
This commit is contained in:
2023-02-25 23:47:28 +02:00
parent 7a2f0b5a0a
commit f0e1aca247
5 changed files with 210 additions and 102 deletions

View File

@ -1,6 +1,6 @@
""" Test cases for the parser """ """ Test cases for the parser """
import pytest import pytest
from ziffers import zparse from ziffers import zparse, collect
# pylint: disable=missing-function-docstring, line-too-long, invalid-name # pylint: disable=missing-function-docstring, line-too-long, invalid-name
@ -52,18 +52,19 @@ def test_parsing_text(pattern: str):
], ],
) )
def test_pitch_classes(pattern: str, expected: list): def test_pitch_classes(pattern: str, expected: list):
assert zparse(pattern).pitch_classes() == expected assert collect(zparse(pattern),len(expected)*2,"pitch_class") == expected*2
@pytest.mark.parametrize( @pytest.mark.parametrize(
"pattern,expected", "pattern,expected",
[ [
("__6 _0 _1 _2 _3 _4 _5 _6 0 1 2 3 4 5 6 ^0 ^1 ^2 ^3 ^4 ^5 ^6 ^^0", [-2, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2]), ("__6 _0 _1 _2 _3 _4 _5 _6 0 1 2 3 4 5 6 ^0 ^1 ^2 ^3 ^4 ^5 ^6 ^^0", [-2, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2]),
("_ 1 _2 <3>3 ^^4", [-1, -1, 3, 2]), ("_ 1 _2 <3>3 ^^4", [-1, -2, 3, 1]),
("^ 1 ^1 3 _2 ^^1 _2 <-1> 2 ^4", [1, 2, 1, 0, 3, 0, -1, 0]),
] ]
) )
def test_pitch_octaves(pattern: str, expected: list): def test_pitch_octaves(pattern: str, expected: list):
assert zparse(pattern).octaves() == expected assert collect(zparse(pattern),len(expected)*2,"octave") == expected*2
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -71,11 +72,11 @@ def test_pitch_octaves(pattern: str, expected: list):
[ [
("w [1 [2 3]]", [0.5, 0.25, 0.25]), ("w [1 [2 3]]", [0.5, 0.25, 0.25]),
("1.0 [1 [2 3]] 4 [3 [4 5]]", [0.5, 0.25, 0.25, 1.0, 0.5, 0.25, 0.25]), ("1.0 [1 [2 3]] 4 [3 [4 5]]", [0.5, 0.25, 0.25, 1.0, 0.5, 0.25, 0.25]),
("0.5 (0 0.25 3)+1", [0.5, 0.5]) ("0.5 (0 0.25 3)+1", [0.5, 0.25])
] ]
) )
def test_subdivisions(pattern: str, expected: list): def test_subdivisions(pattern: str, expected: list):
assert zparse(pattern).durations() == expected assert collect(zparse(pattern),len(expected)*2,"duration") == expected*2
@pytest.mark.parametrize( @pytest.mark.parametrize(
"pattern,expected", "pattern,expected",
@ -85,7 +86,7 @@ def test_subdivisions(pattern: str, expected: list):
] ]
) )
def test_repeats(pattern: str, expected: list): def test_repeats(pattern: str, expected: list):
assert zparse(pattern).notes() == expected assert collect(zparse(pattern),len(expected)*2,"note") == expected*2
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -104,3 +105,4 @@ def test_looping_durations(pattern: str, expected: list):
for i in range(12): for i in range(12):
durations.append(parsed[i].duration) durations.append(parsed[i].duration)
assert durations == expected assert durations == expected

View File

@ -15,6 +15,7 @@ class Meta:
"""Abstract class for all Ziffers items""" """Abstract class for all Ziffers items"""
kwargs: dict = field(default=None, repr=False) kwargs: dict = field(default=None, repr=False)
local_options: dict = field(default_factory=dict)
def __post_init__(self): def __post_init__(self):
if self.kwargs: if self.kwargs:
@ -26,12 +27,26 @@ class Meta:
if hasattr(self, key): if hasattr(self, key):
setattr(self, key, value) setattr(self, key, value)
def update_options(self, new_values): def update_options(self, options):
"""Updates attribute values only if value is None""" """Updates attribute values only if value is None"""
for key, value in new_values.items(): merged_options = self.local_options | options
for key, value in merged_options.items():
if hasattr(self, key): if hasattr(self, key):
if getattr(self, key) is None: if key == "octave":
setattr(self, key, value) local_value = self.local_options.get("octave", False)
oct_change = self.local_options.get("octave_change", False)
if oct_change:
setattr(self, key, oct_change)
elif local_value:
setattr(self, key, value + local_value)
else:
setattr(self, key, value)
elif getattr(self, key) is None:
local_value = self.local_options.get(key, False)
if local_value:
setattr(self, key, local_value)
else:
setattr(self, key, value)
def dict(self): def dict(self):
"""Returns safe dict from the dataclass""" """Returns safe dict from the dataclass"""
@ -56,6 +71,14 @@ class Item(Meta):
self.replace_options(options) self.replace_options(options)
return self return self
def get_options(self) -> dict:
"""Return local options from item
Returns:
dict: Options as a dict
"""
keys = ["octave","modifier","key","scale","duration"]
return {key: getattr(self, key) for key in keys if hasattr(self, key)}
@dataclass(kw_only=True) @dataclass(kw_only=True)
class Whitespace: class Whitespace:
@ -118,17 +141,17 @@ class Pitch(Event):
pitch_class: int pitch_class: int
octave: int = field(default=None) octave: int = field(default=None)
modifier: int = field(default=0) modifier: int = field(default=None)
note: int = field(default=None) note: int = field(default=None)
key: str = field(default=None) key: str = field(default=None)
scale: str | list = field(default=None) scale: str | list = field(default=None)
freq: float = field(default=None) freq: float = field(default=None)
beat: float = field(default=None)
def __post_init__(self): def __post_init__(self):
super().__post_init__() super().__post_init__()
if self.text is None: if self.text is None:
self.text = str(self.pitch_class) self.text = str(self.pitch_class)
self.update_note() self.update_note()
def update_note(self, force: bool = False): def update_note(self, force: bool = False):
"""Update note if Key, Scale and Pitch-class are present""" """Update note if Key, Scale and Pitch-class are present"""
@ -147,6 +170,8 @@ class Pitch(Event):
) )
self.freq = midi_to_freq(note) self.freq = midi_to_freq(note)
self.note = note self.note = note
if self.duration is not None:
self.beat = self.duration*4
def check_note(self, options: dict): def check_note(self, options: dict):
"""Check for note modification""" """Check for note modification"""
@ -302,16 +327,20 @@ class Sequence(Meta):
def __post_init__(self): def __post_init__(self):
super().__post_init__() super().__post_init__()
self.text = self.__collect_text() self.text = self.__collect_text()
self.update_local_options()
def __getitem__(self, index): def __getitem__(self, index):
return self.values[index] return self.values[index]
def update_values(self, new_values): def update_local_options(self):
"""Update value attributes from dict""" """Update value attributes from dict"""
for key, value in new_values.items(): if self.local_options:
for obj in self.values: for obj in self.values:
if key != "text" and hasattr(obj, key): if isinstance(obj, Event):
setattr(obj, key, value) if obj.local_options:
obj.local_options = obj.local_options | self.local_options.copy()
else:
obj.local_options = self.local_options.copy()
def __collect_text(self) -> str: def __collect_text(self) -> str:
"""Collect text value from values""" """Collect text value from values"""
@ -337,18 +366,12 @@ class Sequence(Meta):
yield from item.evaluate(options) yield from item.evaluate(options)
elif isinstance(item, RepeatedSequence): elif isinstance(item, RepeatedSequence):
item.evaluate_values(options) item.evaluate_values(options)
# TODO: Refactor this. Parsing and validating cycles for integers only? repeats = item.repeats.get_value(options)
if isinstance(item.repeats, Cyclic): repeats = _resolve_repeat_value(repeats)
repeats = item.repeats.get_value().get_value(options)
else:
repeats = item.repeats.get_value(options)
yield from _normal_repeat(item.evaluated_values, repeats, options) yield from _normal_repeat(item.evaluated_values, repeats, options)
elif isinstance(item, RepeatedListSequence): elif isinstance(item, RepeatedListSequence):
repeats = item.repeats.get_value(options) repeats = item.repeats.get_value(options)
while isinstance(repeats, Cyclic): repeats = _resolve_repeat_value(repeats)
repeats = item.repeats.get_value(options)
if isinstance(repeats, Pitch):
repeats = repeats.get_value(options)
yield from _generative_repeat(item, repeats, options) yield from _generative_repeat(item, repeats, options)
elif isinstance(item, Subdivision): elif isinstance(item, Subdivision):
item.evaluate_values(options) item.evaluate_values(options)
@ -375,17 +398,26 @@ class Sequence(Meta):
elif isinstance(item, Euclid): elif isinstance(item, Euclid):
yield from _euclidean_items(item, options) yield from _euclidean_items(item, options)
elif isinstance(item, Modification): elif isinstance(item, Modification):
options = _update_options(item, options) options = _parse_options(item, options)
elif isinstance(item, Meta): # Filters whitespace elif isinstance(item, Meta): # Filters whitespace
yield _update_item(item, options) yield _update_item(item, options)
def _resolve_repeat_value(item):
while isinstance(item, Cyclic):
item = item.get_value(options)
if isinstance(item, Pitch):
return item.get_value(options)
if not isinstance(item, Integer):
return 2
return item
def _update_item(item, options): def _update_item(item, options):
"""Update or create new pitch""" """Update or create new pitch"""
if set(("key", "scale")) <= options.keys(): if set(("key", "scale")) <= options.keys():
if isinstance(item, Pitch): if isinstance(item, Pitch):
item.update_options(options) item.update_options(options)
item.update_note() item.update_note()
if options.get("pre_eval",False): if options.get("pre_eval", False):
item.duration = options["duration"] item.duration = options["duration"]
if isinstance(item, Rest): if isinstance(item, Rest):
item.update_options(options) item.update_options(options)
@ -422,7 +454,7 @@ class Sequence(Meta):
for item in items: for item in items:
yield from _resolve_item(item, options) yield from _resolve_item(item, options)
def _update_options(current: Item, options: dict) -> dict: def _parse_options(current: Item, options: dict) -> dict:
"""Update options based on current item""" """Update options based on current item"""
if isinstance(current, (OctaveChange, DurationChange)): if isinstance(current, (OctaveChange, DurationChange)):
options[current.key] = current.value options[current.key] = current.value
@ -439,26 +471,32 @@ class Sequence(Meta):
def _create_pitch(current: Item, options: dict) -> Pitch: def _create_pitch(current: Item, options: dict) -> Pitch:
"""Create pitch based on values and options""" """Create pitch based on values and options"""
if "modifier" in options: merged_options = options | self.local_options
c_modifier = options["modifier"]
if "modifier" in merged_options:
c_modifier = merged_options["modifier"]
else: else:
c_modifier = 0 c_modifier = 0
if hasattr(current, "modifier") and current.modifier is not None: if hasattr(current, "modifier") and current.modifier is not None:
c_modifier += current.modifier c_modifier += current.modifier
if "octave" in options: if "octave" in merged_options:
c_octave = options["octave"] c_octave = merged_options["octave"]
if "octave" in options:
c_octave = options["octave"] + c_octave
else: else:
c_octave = 0 c_octave = 0
if hasattr(current, "octave") and current.octave is not None: if hasattr(current, "octave") and current.octave is not None:
c_octave += current.octave c_octave += current.octave
current_value = current.get_value(options)
current_value = current.get_value(merged_options)
note = note_from_pc( note = note_from_pc(
root=options["key"], root=merged_options["key"],
pitch_class=current_value, pitch_class=current_value,
intervals=options["scale"], intervals=merged_options["scale"],
modifier=c_modifier, modifier=c_modifier,
octave=c_octave, octave=c_octave,
) )
@ -469,7 +507,7 @@ class Sequence(Meta):
freq=midi_to_freq(note), freq=midi_to_freq(note),
octave=c_octave, octave=c_octave,
modifier=c_modifier, modifier=c_modifier,
kwargs=options, kwargs=merged_options,
) )
return new_pitch return new_pitch
@ -542,7 +580,7 @@ class Ziffers(Sequence):
self.loop_i = index % self.cycle_length self.loop_i = index % self.cycle_length
new_cycle = floor(index / self.cycle_length) new_cycle = floor(index / self.cycle_length)
if new_cycle > self.cycle_i or new_cycle < self.cycle_i: if new_cycle > self.cycle_i or new_cycle < self.cycle_i:
self.re_eval(self.options) self.re_eval()
self.cycle_i = new_cycle self.cycle_i = new_cycle
self.cycle_length = len(self.evaluated_values) self.cycle_length = len(self.evaluated_values)
self.loop_i = index % self.cycle_length self.loop_i = index % self.cycle_length
@ -568,11 +606,9 @@ class Ziffers(Sequence):
self.start_options = self.options.copy() self.start_options = self.options.copy()
self.init_tree(self.options) self.init_tree(self.options)
def re_eval(self, options=None): def re_eval(self):
"""Re-evaluate the iterator""" """Re-evaluate the iterator"""
self.options = self.start_options.copy() self.options = self.start_options.copy()
if options:
self.options.update(options)
self.init_tree(self.options) self.init_tree(self.options)
def init_tree(self, options): def init_tree(self, options):
@ -631,6 +667,10 @@ class Ziffers(Sequence):
"""Return list of pitch durations as floats""" """Return list of pitch durations as floats"""
return [val.duration for val in self.evaluated_values if isinstance(val, Event)] return [val.duration for val in self.evaluated_values if isinstance(val, Event)]
def beats(self) -> list[float]:
"""Return list of pitch durations as floats"""
return [val.beat for val in self.evaluated_values if isinstance(val, Event)]
def pairs(self) -> list[tuple]: def pairs(self) -> list[tuple]:
"""Return list of pitches and durations""" """Return list of pitches and durations"""
return [ return [
@ -757,14 +797,17 @@ class Range(Item):
def evaluate(self, options): def evaluate(self, options):
"""Evaluates range and generates a generator of Pitches""" """Evaluates range and generates a generator of Pitches"""
merged_options = options | self.local_options
if options["octave"]:
merged_options["octave"] += options["octave"]
if self.start < self.end: if self.start < self.end:
for i in range(self.start, self.end + 1): for i in range(self.start, self.end + 1):
yield Pitch(pitch_class=i, kwargs=options) yield Pitch(pitch_class=i, local_options=merged_options)
elif self.start > self.end: elif self.start > self.end:
for i in reversed(range(self.end, self.start + 1)): for i in reversed(range(self.end, self.start + 1)):
yield Pitch(pitch_class=i, kwargs=options) yield Pitch(pitch_class=i, local_options=merged_options)
else: else:
yield Pitch(pitch_class=self.start, kwargs=options) yield Pitch(pitch_class=self.start, local_options=merged_options)
@dataclass(kw_only=True) @dataclass(kw_only=True)
@ -783,25 +826,27 @@ class ListOperation(Sequence):
def evaluate(self, options=DEFAULT_OPTIONS.copy()): def evaluate(self, options=DEFAULT_OPTIONS.copy()):
"""Evaluates the operation""" """Evaluates the operation"""
def filter_operation(input_list): def filter_operation(input_list, options):
flattened_list = [] flattened_list = []
# TODO: ADD Options here and evaluate events?
for item in input_list: for item in input_list:
if isinstance(item, (list, Sequence)): if isinstance(item, (list, Sequence)):
if isinstance(item, ListOperation): if isinstance(item, ListOperation):
flattened_list.extend(item.evaluated_values) flattened_list.extend(item.evaluated_values)
else: else:
flattened_list.append(filter_operation(item)) flattened_list.append(filter_operation(item, options))
elif isinstance(item, Cyclic): elif isinstance(item, Cyclic):
value = item.get_value() value = item.get_value()
if isinstance(value, Sequence): if isinstance(value, Sequence):
flattened_list.extend(filter_operation(value)) flattened_list.extend(filter_operation(value, options))
elif isinstance(value, (Event, RandomInteger, Integer)): elif isinstance(value, (Event, RandomInteger, Integer)):
flattened_list.append(value) flattened_list.append(value)
elif isinstance(item, Modification):
options = options | item.as_options()
elif isinstance(item, Range): elif isinstance(item, Range):
flattened_list.extend(list(item.evaluate(options))) flattened_list.extend(list(item.evaluate(options)))
elif isinstance(item, (Event, RandomInteger, Integer)): elif isinstance(item, (Event, RandomInteger, Integer)):
item.update_options(options)
flattened_list.append(item) flattened_list.append(item)
if isinstance(input_list, Sequence): if isinstance(input_list, Sequence):
@ -811,7 +856,7 @@ class ListOperation(Sequence):
operators = self.values[1::2] # Fetch every second operator element operators = self.values[1::2] # Fetch every second operator element
values = self.values[::2] # Fetch every second list element values = self.values[::2] # Fetch every second list element
values = filter_operation(values) # Filter out values = filter_operation(values, options) # Filter out
if len(values) == 1: if len(values) == 1:
return values[0] # If right hand doesnt contain anything sensible return values[0] # If right hand doesnt contain anything sensible
left = values[0] # Start results with the first array left = values[0] # Start results with the first array
@ -823,10 +868,9 @@ class ListOperation(Sequence):
(right.values if isinstance(right, Sequence) else [right]), left (right.values if isinstance(right, Sequence) else [right]), left
) )
left = [ left = [
# TODO: Get options from x value?
Pitch( Pitch(
pitch_class=operation(x.get_value(options), y.get_value(options)), pitch_class=operation(x.get_value(options), y.get_value(options)),
kwargs=options, kwargs=y.get_options()
) )
for (x, y) in pairs for (x, y) in pairs
] ]
@ -911,7 +955,6 @@ class RepeatedSequence(Sequence):
repeats: RandomInteger | Integer = field(default_factory=Integer(value=1, text="1")) repeats: RandomInteger | Integer = field(default_factory=Integer(value=1, text="1"))
wrap_start: str = field(default="[:", repr=False) wrap_start: str = field(default="[:", repr=False)
wrap_end: str = field(default=":]", repr=False) wrap_end: str = field(default=":]", repr=False)
local_options: dict = field(default_factory=dict, init=False)
evaluated_values: list = None evaluated_values: list = None

View File

@ -1,5 +1,4 @@
""" Lark transformer for mapping Lark tokens to Ziffers objects """ """ Lark transformer for mapping Lark tokens to Ziffers objects """
from typing import Optional
from lark import Transformer from lark import Transformer
from .classes import ( from .classes import (
Ziffers, Ziffers,
@ -29,7 +28,7 @@ from .classes import (
Euclid, Euclid,
RepeatedSequence, RepeatedSequence,
VariableAssignment, VariableAssignment,
Variable Variable,
) )
from .common import flatten, sum_dict from .common import flatten, sum_dict
from .defaults import DEFAULT_DURS, OPERATORS from .defaults import DEFAULT_DURS, OPERATORS
@ -50,51 +49,82 @@ class ZiffersTransformer(Transformer):
def rest(self, items): def rest(self, items):
"""Return duration event""" """Return duration event"""
if len(items)>0: if len(items) > 0:
chars = items[0] prefixes = sum_dict(items)
val = DEFAULT_DURS[chars[0]] text_prefix = prefixes.pop("text")
# TODO: Add support for dots prefixes["prefix"] = text_prefix
#if len(chars)>1: return Rest(text=text_prefix + "r", local_options=prefixes)
# dots = len(chars)-1
# val = val * (2.0 - (1.0 / (2 * dots)))
return Rest(text=chars+"r", duration=val)
return Rest(text="r") return Rest(text="r")
return Rest(text=chars+"r", duration=val) def random_integer(self, items) -> RandomInteger:
"""Parses random integer syntax"""
if len(items) > 1:
prefixes = sum_dict(items[0:-1]) # If there are prefixes
text_prefix = prefixes.pop("text")
prefixes["prefix"] = text_prefix
val = items[-1][1:-1].split(",")
return RandomInteger(
min=int(val[0]),
max=int(val[1]),
text=text_prefix + items[-1],
local_options=prefixes,
)
else:
val = items[0][1:-1].split(",")
return RandomInteger(min=int(val[0]), max=int(val[1]), text=items[0])
def rest_duration(self,items): def random_integer_re(self, items):
"""Return random integer notation from regex"""
return items[0].value return items[0].value
def random_integer(self, item) -> RandomInteger: def range(self, items) -> Range:
"""Parses random integer syntax"""
val = item[0][1:-1].split(",")
return RandomInteger(min=int(val[0]), max=int(val[1]), text=item[0].value)
def range(self, item) -> Range:
"""Parses range syntax""" """Parses range syntax"""
val = item[0].split("..") if len(items) > 1:
return Range(start=int(val[0]), end=int(val[1]), text=item[0].value) prefixes = sum_dict(items[0:-1]) # If there are prefixes
text_prefix = prefixes.pop("text")
prefixes["prefix"] = text_prefix
val = items[-1].split("..")
return Range(
start=int(val[0]),
end=int(val[1]),
text=text_prefix + items[-1],
local_options=prefixes,
)
# Else
val = items[0].split("..")
return Range(start=int(val[0]), end=int(val[1]), text=items[0])
def range_re(self, items):
"""Return range value from regex"""
return items[0].value
def cycle(self, items) -> Cyclic: def cycle(self, items) -> Cyclic:
"""Parses cycle""" """Parses cycle"""
values = items[0] values = items[0]
return Cyclic(values=values) return Cyclic(values=values)
def pitch_class(self, item): def pitch_class(self, items):
"""Parses pitch class""" """Parses pitch class"""
# If there are prefixes # If there are prefixes
if len(item) > 1: if len(items) > 1:
# Collect&sum prefixes from any order: _qee^s4 etc. # Collect&sum prefixes from any order: _qee^s4 etc.
result = sum_dict(item) prefixes = sum_dict(items[0:-1]) # If there are prefixes
return Pitch(**result) text_prefix = prefixes.pop("text")
prefixes["prefix"] = text_prefix
p = Pitch(
pitch_class=items[-1]["pitch_class"],
text=text_prefix + items[-1]["text"],
local_options=prefixes,
)
return p
val = item[0] val = items[0]
return Pitch(**val) return Pitch(**val)
def pitch(self, items): def pitch(self, items):
"""Return pitch class info""" """Return pitch class info"""
text_value = items[0].value.replace("T","10").replace("E","11") text_value = items[0].value.replace("T", "10").replace("E", "11")
return {"pitch_class": int(text_value), "text": items[0].value} return {"pitch_class": int(text_value), "text": items[0].value}
def prefix(self, items): def prefix(self, items):
@ -104,7 +134,10 @@ class ZiffersTransformer(Transformer):
def oct_change(self, items): def oct_change(self, items):
"""Parses octave change""" """Parses octave change"""
octave = items[0] octave = items[0]
return [OctaveChange(value=octave["octave"], text=octave["text"]), items[1]] return [
OctaveChange(value=octave["octave_change"], text=octave["text"]),
items[1],
]
def oct_mod(self, items): def oct_mod(self, items):
"""Parses octave modification""" """Parses octave modification"""
@ -114,7 +147,7 @@ class ZiffersTransformer(Transformer):
def escaped_octave(self, items): def escaped_octave(self, items):
"""Return octave info""" """Return octave info"""
value = items[0][1:-1] value = items[0][1:-1]
return {"octave": int(value), "text": items[0].value} return {"octave_change": int(value), "text": items[0].value}
def octave(self, items): def octave(self, items):
"""Return octaves ^ and _""" """Return octaves ^ and _"""
@ -124,7 +157,7 @@ class ZiffersTransformer(Transformer):
def modifier(self, items): def modifier(self, items):
"""Return modifiers # and b""" """Return modifiers # and b"""
value = 1 if items[0].value == "#" else -1 value = 1 if items[0].value == "#" else -1
return {"modifier": value} return {"modifier": value, "text": items[0].value}
def chord(self, items): def chord(self, items):
"""Parses chord""" """Parses chord"""
@ -229,9 +262,7 @@ class ZiffersTransformer(Transformer):
def subdivision(self, items): def subdivision(self, items):
"""Parse subdivision""" """Parse subdivision"""
values = flatten(items[0]) values = flatten(items[0])
return Subdivision( return Subdivision(values=values, wrap_start="[", wrap_end="]")
values=values, wrap_start="[", wrap_end="]"
)
def subitems(self, items): def subitems(self, items):
"""Return subdivision items""" """Return subdivision items"""
@ -257,19 +288,26 @@ class ZiffersTransformer(Transformer):
val = token[0].value val = token[0].value
return Atom(value=val, text=val) return Atom(value=val, text=val)
# Variable assignment # Variable assignment
def assignment(self, items): def assignment(self, items):
"""Creates variable assignment"""
var = items[0] var = items[0]
op = items[1] op = items[1]
content = items[2] content = items[2]
return VariableAssignment(variable=var, value=content, text=var.text+"="+content.text, pre_eval=True if op == "=" else False) return VariableAssignment(
variable=var,
value=content,
text=var.text + "=" + content.text,
pre_eval=True if op == "=" else False,
)
def ass_op(self,items): def ass_op(self, items):
"""Return parsed type for assignment: = or ~"""
return items[0].value return items[0].value
def variable(self, items): def variable(self, items):
"""Return parsed variable name"""
return Variable(name=items[0].value, text=items[0].value) return Variable(name=items[0].value, text=items[0].value)
# List rules # List rules
@ -278,9 +316,14 @@ class ZiffersTransformer(Transformer):
"""Parse list sequence notation, ex: (1 2 3)""" """Parse list sequence notation, ex: (1 2 3)"""
if len(items) > 1: if len(items) > 1:
prefixes = sum_dict(items[0:-1]) prefixes = sum_dict(items[0:-1])
text_prefix = prefixes.pop("text")
prefixes["prefix"] = text_prefix
values = items[-1] values = items[-1]
seq = ListSequence(values=values, wrap_start=prefixes["text"] + "(") seq = ListSequence(
seq.update_values(prefixes) values=values,
wrap_start=prefixes["prefix"] + "(",
local_options=prefixes,
)
return seq return seq
else: else:
seq = ListSequence(values=items[0]) seq = ListSequence(values=items[0])
@ -295,12 +338,13 @@ class ZiffersTransformer(Transformer):
values=items[-2], values=items[-2],
repeats=items[-1], repeats=items[-1],
wrap_end=":" + items[-1].text + ")", wrap_end=":" + items[-1].text + ")",
local_options=prefixes,
) )
else: else:
seq = RepeatedListSequence( seq = RepeatedListSequence(
values=items[-2], repeats=Integer(text="2", value=2) values=items[-2],
repeats=Integer(text="2", value=2, local_options=prefixes),
) )
seq.update_values(prefixes)
return seq return seq
else: else:
if items[-1] is not None: if items[-1] is not None:
@ -351,7 +395,7 @@ class ZiffersTransformer(Transformer):
"""Parse list operation""" """Parse list operation"""
return ListOperation(values=items) return ListOperation(values=items)
def right_op(self,items): def right_op(self, items):
"""Get right value for the operation""" """Get right value for the operation"""
return items[0] return items[0]
@ -382,4 +426,10 @@ class ZiffersTransformer(Transformer):
return RepeatedSequence(values=items[0], repeats=Integer(value=2, text="2")) return RepeatedSequence(values=items[0], repeats=Integer(value=2, text="2"))
def repeat_item(self, items): def repeat_item(self, items):
return RepeatedListSequence(values=[items[0]],repeats=items[1], wrap_start="", wrap_end=":"+items[1].text) """Parse repeat item syntax to sequence, ex: 1:4 (1 2 3):5"""
return RepeatedListSequence(
values=[items[0]],
repeats=items[1],
wrap_start="",
wrap_end=":" + items[1].text,
)

View File

@ -52,3 +52,17 @@ def zparse(expr: str, **opts) -> Ziffers:
def z(expr: str, **opts) -> Ziffers: def z(expr: str, **opts) -> Ziffers:
"""Shortened method name for zparse""" """Shortened method name for zparse"""
return zparse(expr, **opts) return zparse(expr, **opts)
def yield_items(gen: Ziffers, num: int, key: str = None) -> list:
"""Yield n items from parsed Ziffers"""
for i in range(num):
if key is not None:
yield getattr(gen[i],key,None)
else:
yield gen[i]
def collect(gen: Ziffers, num: int, key: str = None) -> list:
"""Collect n-item from parsed Ziffers"""
return list(yield_items(gen,num,key))

View File

@ -17,16 +17,13 @@
variable: /[A-Z]/ variable: /[A-Z]/
// Durations // Durations
// TODO: Refactor dchar as: /([mklpdcwyhnqaefsxtgujzo](\.)*)(?=\d)/
duration_chars: dotted_dur+ duration_chars: dotted_dur+
dotted_dur: dchar dot* dotted_dur: dchar dot*
decimal: /-?[0-9]+\.[0-9]+/ decimal: /-?[0-9]+\.[0-9]+/
dchar: /[mklpdcwyhnqaefsxtgujzo]/ dchar: /[mklpdcwyhnqaefsxtgujzo]/
dot: "." dot: "."
rest: rest_duration? "r" rest: prefix* "r"
// TODO: Refactor (\.)* when other durchars uses lookaheads
rest_duration: /([mklpdcwyhnqaefsxtgujzo])(?=r)/
// Chords // Chords
chord: pitch_class pitch_class+ chord: pitch_class pitch_class+
@ -42,7 +39,7 @@
// Repeats // Repeats
repeat: "[:" sequence ":" [number] "]" repeat: "[:" sequence ":" [number] "]"
repeat_item: (pitch_class | list | random_integer | cycle | rest | subdivision) ":" number repeat_item: (pitch_class | list | list_op | random_integer | cycle | rest | subdivision | chord | named_roman | variable | range) ":" number
// List // List
list: prefix* "(" sequence ")" list: prefix* "(" sequence ")"
@ -71,11 +68,13 @@
oct_change: escaped_octave WS oct_change: escaped_octave WS
dur_change: (decimal | char_change) dur_change: (decimal | char_change)
char_change: dchar_not_prefix+ char_change: dchar_not_prefix+
dchar_not_prefix: /([mklpdcwyhnqaefsxtgujzo](\.)*)(?![\dr])/ dchar_not_prefix: /([mklpdcwyhnqaefsxtgujzo](\.)*)(?=[ >])/
// Generative rules // Generative rules
random_integer: /\(-?[0-9]+,-?[0-9]+\)/ random_integer: prefix* random_integer_re
range: /-?[0-9]+\.\.-?[0-9]+/ random_integer_re: /\(-?[0-9]+,-?[0-9]+\)/
range: prefix* range_re
range_re: /-?[0-9]+\.\.-?[0-9]+/
cycle: "<" sequence ">" cycle: "<" sequence ">"
random_pitch: /(\?)(?!\d)/ random_pitch: /(\?)(?!\d)/
random_percent: /(%)(?!\d)/ random_percent: /(%)(?!\d)/