mvp random generator & list operation evaluator

This commit is contained in:
2023-02-12 00:21:21 +02:00
parent e04d202ad5
commit 707e700e37
9 changed files with 387 additions and 190 deletions

View File

@ -39,4 +39,4 @@ if __name__ == "__main__":
print("Parsed: " + parse_expression(expressions[ex]).text) print("Parsed: " + parse_expression(expressions[ex]).text)
except Exception as e: except Exception as e:
print(f"[red]Failed on {ex}[/red]") print(f"[red]Failed on {ex}[/red]")
# print(f"[red]Failed on {ex}[/red]: {str(e)[0:40]}...") # print(f"[red]Failed on {ex}[/red]: {str(e)[0:40]}...")

View File

@ -46,8 +46,8 @@ def test_parsing_text(pattern: str):
("q2 eq3", [2, 3]), ("q2 eq3", [2, 3]),
], ],
) )
def test_pcs(pattern: str, expected: list): def test_pitch_classes(pattern: str, expected: list):
assert parse_expression(pattern).pcs() == expected assert parse_expression(pattern).pitch_classes() == expected
# TODO: Add tests for octaves # TODO: Add tests for octaves
# ("__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]),

View File

@ -15,11 +15,11 @@ from ziffers import scale
], ],
) )
def test_notenames(name: str, expected: int): def test_notenames(name: str, expected: int):
assert scale.note_to_midi(name) == expected assert scale.note_name_to_midi(name) == expected
@pytest.mark.parametrize( @pytest.mark.parametrize(
"pcs,expected", "pitch_classes,expected",
[ [
( (
list(range(-9, 10)), list(range(-9, 10)),
@ -47,7 +47,7 @@ def test_notenames(name: str, expected: int):
), ),
], ],
) )
def test_note_to_midi(pcs: str, expected: int): def test_note_to_midi(pitch_classes: str, expected: int):
assert [ assert [
scale.note_from_pc(root=60, pitch_class=val, intervals="Ionian") for val in pcs scale.note_from_pc(root=60, pitch_class=val, intervals="Ionian") for val in pitch_classes
] == expected ] == expected

View File

@ -1,5 +1,5 @@
""" Ziffers classes for the parsed notation """ """ Ziffers classes for the parsed notation """
from dataclasses import dataclass, field from dataclasses import dataclass, field, replace
import itertools import itertools
import operator import operator
import random import random
@ -7,10 +7,17 @@ from .defaults import DEFAULT_OPTIONS
from .scale import note_from_pc, midi_to_pitch_class from .scale import note_from_pc, midi_to_pitch_class
@dataclass @dataclass(kw_only=True)
class Meta: class Meta:
"""Abstract class for all Ziffers items""" """Abstract class for all Ziffers items"""
kwargs: dict = field(default=None, repr=False)
def __post_init__(self):
if self.kwargs:
for key, val in self.kwargs.items():
setattr(self, key, val)
def update(self, new_values): def update(self, new_values):
"""Update attributes from dict""" """Update attributes from dict"""
for key, value in new_values.items(): for key, value in new_values.items():
@ -25,21 +32,30 @@ class Meta:
setattr(self, key, value) setattr(self, key, value)
@dataclass @dataclass(kw_only=True)
class Item(Meta): class Item(Meta):
"""Class for all Ziffers text based items""" """Class for all Ziffers text based items"""
text: str text: str = field(default=None)
def get_item(self):
"""Return the item"""
return self
@dataclass @dataclass(kw_only=True)
class Whitespace(Item): class Whitespace:
"""Class for whitespace""" """Class for whitespace"""
text: str
item_type: str = field(default=None, repr=False, init=False) item_type: str = field(default=None, repr=False, init=False)
def get_item(self):
"""Returns None. Used in filtering"""
return None
@dataclass
@dataclass(kw_only=True)
class DurationChange(Item): class DurationChange(Item):
"""Class for changing duration""" """Class for changing duration"""
@ -57,7 +73,7 @@ class OctaveChange(Item):
item_type: str = field(default="change", repr=False, init=False) item_type: str = field(default="change", repr=False, init=False)
@dataclass @dataclass(kw_only=True)
class OctaveAdd(Item): class OctaveAdd(Item):
"""Class for modifying octave""" """Class for modifying octave"""
@ -66,42 +82,94 @@ class OctaveAdd(Item):
item_type: str = field(default="add", repr=False, init=False) item_type: str = field(default="add", repr=False, init=False)
@dataclass @dataclass(kw_only=True)
class Event(Item): class Event(Item):
"""Abstract class for events with duration""" """Abstract class for events with duration"""
duration: float = field(default=None) duration: float = field(default=None)
@dataclass @dataclass(kw_only=True)
class Pitch(Event): class Pitch(Event):
"""Class for pitch in time""" """Class for pitch in time"""
pitch_class: int = field(default=None) pitch_class: int
octave: int = field(default=None) octave: int = field(default=None)
modifier: int = field(default=0) modifier: int = field(default=0)
note: int = field(default=None) note: int = field(default=None)
key: str = field(default=None)
scale: str | list = field(default=None)
def set_note(self, note: int): def __post_init__(self):
super().__post_init__()
if self.text is None:
self.text = str(self.pitch_class)
self.update_note()
def update_note(self):
"""Update note if Key, Scale and Pitch-class is present
"""
if (
(self.key is not None)
and (self.scale is not None)
and (self.pitch_class is not None)
and (self.note is None)
):
note = note_from_pc(
root=self.key,
pitch_class=self.pitch_class,
intervals=self.scale,
modifier=self.modifier if self.modifier is not None else 0,
octave=self.octave if self.octave is not None else 0,
)
self.set_note(note)
def set_note(self, note: int) -> int:
"""Sets a note for the pitch and returns the note.
Args:
note (int): Midi note
Returns:
int: Returns the saved note
"""
self.note = note self.note = note
return note return note
# pylint: disable=locally-disabled, unused-argument
def get_value(self, re_eval=False) -> int:
"""Returns the pitch class
@dataclass Returns:
int: Integer value for the pitch
"""
return self.pitch_class
@dataclass(kw_only=True)
class RandomPitch(Event): class RandomPitch(Event):
"""Class for random pitch""" """Class for random pitch"""
pitch_class: int = field(default=None) pitch_class: int = field(default=None)
# pylint: disable=locally-disabled, unused-argument
def get_value(self, re_eval=False) -> int:
"""Return random value
@dataclass Returns:
int: Returns random pitch
"""
return self.pitch_class
@dataclass(kw_only=True)
class RandomPercent(Item): class RandomPercent(Item):
"""Class for random percent""" """Class for random percent"""
percent: float = field(default=None) percent: float = field(default=None)
@dataclass @dataclass(kw_only=True)
class Chord(Event): class Chord(Event):
"""Class for chords""" """Class for chords"""
@ -113,7 +181,7 @@ class Chord(Event):
self.notes = notes self.notes = notes
@dataclass @dataclass(kw_only=True)
class RomanNumeral(Event): class RomanNumeral(Event):
"""Class for roman numbers""" """Class for roman numbers"""
@ -123,46 +191,49 @@ class RomanNumeral(Event):
pitch_classes: list = None pitch_classes: list = None
def set_notes(self, chord_notes: list[int]): def set_notes(self, chord_notes: list[int]):
"""Set notes to roman numeral
Args:
chord_notes (list[int]): List of notes in midi to be added
"""
self.notes = chord_notes self.notes = chord_notes
def set_pitch_classes(self, pitches: list[tuple]): def set_pitch_classes(self, pitches: list[tuple]):
if self.pitch_classes == None: """Set pitch classes to roman numeral
Args:
pitches (list[tuple]): Pitch classes to be added
"""
if self.pitch_classes is None:
self.pitch_classes = [] self.pitch_classes = []
for pitch in pitches: for pitch in pitches:
self.pitch_classes.append(Pitch(**pitch)) self.pitch_classes.append(Pitch(**pitch))
@dataclass @dataclass(kw_only=True)
class Function(Event): class Function(Event):
"""Class for functions""" """Class for functions"""
run: str = field(default=None) run: str = field(default=None)
@dataclass @dataclass(kw_only=True)
class Sequence(Meta): class Sequence(Meta):
"""Class for sequences of items""" """Class for sequences of items"""
values: list[Item] values: list
text: str = field(default=None) text: str = field(default=None)
wrap_start: str = field(default=None, repr=False) wrap_start: str = field(default=None, repr=False)
wrap_end: str = field(default=None, repr=False) wrap_end: str = field(default=None, repr=False)
local_index: int = field(default=0, init=False) local_index: int = field(default=0, init=False)
evaluation: bool = field(default=False, init=False)
def __post_init__(self): def __post_init__(self):
super().__post_init__()
self.text = self.__collect_text() self.text = self.__collect_text()
def __iter__(self): def __getitem__(self, index):
return self return self.values[index]
def __next__(self):
if self.local_index < len(self.values):
next_item = self.values[self.local_index]
self.local_index += 1
return next_item
self.local_index = 0
raise StopIteration
def update_values(self, new_values): def update_values(self, new_values):
"""Update value attributes from dict""" """Update value attributes from dict"""
@ -180,77 +251,183 @@ class Sequence(Meta):
text = text + self.wrap_end text = text + self.wrap_end
return text return text
def flatten_values(self): def evaluate_tree(self, options=None, re_eval=False):
"""Flattens the Ziffers object tree""" """Evaluates and flattens the Ziffers object tree"""
for item in self.values: for item in self.values:
if isinstance(item, Sequence): if isinstance(item, Sequence):
yield from item.flatten_values() if item.evaluation:
yield from item.evaluate(options, re_eval)
else:
yield from item.evaluate_tree(options, re_eval)
else: else:
yield item # Get value / generated value from the item
current = item.get_item()
# Ignore items that returns None
if current is not None:
if isinstance(current, (DurationChange, OctaveChange, OctaveAdd)):
options = self.__update_options(current, options)
else:
if set(("key", "scale")) <= options.keys():
if isinstance(current, (Pitch, RandomPitch, RandomInteger)):
current = self.__update_pitch(current, options, re_eval)
elif isinstance(current, Chord):
current = self.__update_chord(current, options)
elif isinstance(current, RomanNumeral):
current = self.__create_chord_from_roman(
current, options
)
current.update_new(options)
yield current
def filter(self, keep: tuple):
"""Filter out items from sequence.
Args:
keep (tuple): Tuple describing classes to keep
Returns:
Sequence: Copy of the sequence with filtered values.
"""
return replace(
self, values=[item for item in self.values if isinstance(item, keep)]
)
def __update_options(self, current: Item, options: dict) -> dict:
"""Update options based on current item
Args:
current (Item): Current item like Duration change, Octave change etc.
options (dict): Current options
Returns:
dict: Updated options
"""
if current.item_type == "change": # Change options
options[current.key] = current.value
elif current.item_type == "add":
if current.key in options: # Add to existing value
options[current.key] += current.value
else: # Create value if not existing
options[current.key] = current.value
return options
def __update_pitch(self, current: Item, options: dict, re_eval: bool = False) -> dict:
"""Update pich based on optons
Args:
current (Item): _description_
options (dict): _description_
Returns:
dict: _description_
"""
if hasattr(current, "modifier"):
c_modifier = 0
elif options["modifier"]:
c_modifier = options["modifier"]
else:
c_modifier = 0
if hasattr(current, "octave"):
c_octave = 0
elif options["octave"]:
c_octave = options["octave"]
else:
c_octave = 0
note = note_from_pc(
root=options["key"],
pitch_class=current.get_value(re_eval),
intervals=options["scale"],
modifier=c_modifier,
octave=c_octave,
)
new_pitch = Pitch(
pitch_class=current.get_value(re_eval),
text=str(current.get_value(re_eval)),
note=note,
octave=c_octave,
modifier=c_modifier,
kwargs=options,
)
return new_pitch
def __update_chord(self, current: Chord, options: dict) -> Chord:
"""Update chord based on options
Args:
current (Chord): Current chord object
options (dict): Options
re (bool, optional): Re-evaluation flag. Defaults to False.
Returns:
Chord: Returns updated chord
"""
pcs = current.pitch_classes
notes = [
pc.set_note(note_from_pc(options["key"], pc.pitch_class, options["scale"]))
for pc in pcs
]
current.set_notes(notes)
return current
def __create_chord_from_roman(self, current: RomanNumeral, options: dict) -> Chord:
"""Create chord fom roman numeral
Args:
current (RomanNumeral): Current roman numeral
options (dict): Options
re (bool, optional): Re-evaluation flag. Defaults to False.
Returns:
Chord: New chord created from Roman numeral
"""
key = options["key"]
scale = options["scale"]
pitches = [midi_to_pitch_class(note, key, scale) for note in current.notes]
chord_notes = [
note_from_pc(
root=key,
pitch_class=pitch,
intervals=scale,
modifier=current.modifier if hasattr(current, "modifier") else 0,
)
for pitch in pitches
]
chord = Chord(text="".join(pitches), pitch_classes=pitches, notes=chord_notes)
return chord
@dataclass @dataclass(kw_only=True)
class Ziffers(Sequence): class Ziffers(Sequence):
"""Main class for holding options and the current state""" """Main class for holding options and the current state"""
options: dict = field(default_factory=DEFAULT_OPTIONS) options: dict = field(default_factory=DEFAULT_OPTIONS)
loop_i: int = 0 loop_i: int = 0
iterator: iter = field(default=None, repr=False) iterator = None
current: Whitespace | DurationChange | OctaveChange | OctaveAdd = field( current: Item = field(default=None)
default=None
)
def __post_init__(self): def __iter__(self):
super().__post_init__() return self
self.iterator = self.flatten_values()
def __next__(self): def __next__(self):
self.current = next(self.iterator) self.current = next(self.iterator)
# Skip whitespace and collect duration & octave changes
while isinstance(
self.current, (Whitespace, DurationChange, OctaveChange, OctaveAdd)
):
if self.current.item_type == "change": # Change options
self.options[self.current.key] = self.current.value
elif self.current.item_type == "add":
if self.current.key in self.options: # Add to existing value
self.options[self.current.key] += self.current.value
else: # Create value if not existing
self.options[self.current.key] = self.current.value
self.current = next(self.iterator) # Skip item
# Update collected options & default options
self.current.update_new(self.options)
# Resolve note(s) from scale
if set(("key", "scale")) <= self.options.keys():
key = self.options["key"]
scale = self.options["scale"]
if isinstance(self.current, (Pitch, RandomPitch)):
note = note_from_pc(
root=key,
pitch_class=self.current.pitch_class,
intervals=scale,
modifier=self.current.modifier,
)
self.current.set_note(note)
elif isinstance(self.current, Chord):
pcs = self.current.pitch_classes
notes = [
pc.set_note(note_from_pc(key, pc.pitch_class, scale)) for pc in pcs
]
self.current.set_notes(notes)
elif isinstance(self.current, RomanNumeral):
pitch_classes = [
midi_to_pitch_class(note, key, scale) for note in self.current.notes
]
self.current.set_pitch_classes(pitch_classes)
self.loop_i += 1 self.loop_i += 1
return self.current return self.current
def init_opts(self, options):
"""Evaluate the Ziffers tree using the options"""
self.options = options
self.iterator = iter(self.evaluate_tree(self.options))
def re_eval(self, options):
"""Re-evaluate the iterator"""
self.iterator = iter(self.evaluate_tree(options, True))
def get_list(self):
"""Return list"""
return list(self)
def take(self, num: int) -> list[Pitch]: def take(self, num: int) -> list[Pitch]:
"""Take number of pitch classes from the parsed sequence. Cycles from the beginning. """Take number of pitch classes from the parsed sequence. Cycles from the beginning.
@ -263,7 +440,8 @@ class Ziffers(Sequence):
return list(itertools.islice(itertools.cycle(self), num)) return list(itertools.islice(itertools.cycle(self), num))
def loop(self) -> iter: def loop(self) -> iter:
return itertools.cycle(self.iterator) """Return cyclic loop"""
return itertools.cycle(iter(self))
def set_defaults(self, options: dict): def set_defaults(self, options: dict):
"""Sets options for the parser """Sets options for the parser
@ -293,7 +471,7 @@ class Ziffers(Sequence):
return [val.octave for val in self.values if isinstance(val, Pitch)] return [val.octave for val in self.values if isinstance(val, Pitch)]
@dataclass @dataclass(kw_only=True)
class ListSequence(Sequence): class ListSequence(Sequence):
"""Class for Ziffers list sequences""" """Class for Ziffers list sequences"""
@ -301,14 +479,19 @@ class ListSequence(Sequence):
wrap_end: str = field(default=")", repr=False) wrap_end: str = field(default=")", repr=False)
@dataclass @dataclass(kw_only=True)
class Integer(Item): class Integer(Item):
"""Class for integers""" """Class for integers"""
value: int value: int
# pylint: disable=locally-disabled, unused-argument
def get_value(self, re_eval=False):
"""Return value of the integer"""
return self.value
@dataclass
@dataclass(kw_only=True)
class RandomInteger(Item): class RandomInteger(Item):
"""Class for random integer""" """Class for random integer"""
@ -316,17 +499,19 @@ class RandomInteger(Item):
max: int max: int
def __post_init__(self): def __post_init__(self):
super().__post_init__()
if self.min > self.max: if self.min > self.max:
new_max = self.min new_max = self.min
self.min = self.max self.min = self.max
self.max = new_max self.max = new_max
def value(self): # pylint: disable=locally-disabled, unused-argument
def get_value(self, re_eval=False):
"""Evaluate the random value for the generator""" """Evaluate the random value for the generator"""
return random.randint(self.min, self.max) return random.randint(self.min, self.max)
@dataclass @dataclass(kw_only=True)
class RepeatedListSequence(Sequence): class RepeatedListSequence(Sequence):
"""Class for Ziffers list sequences""" """Class for Ziffers list sequences"""
@ -335,32 +520,47 @@ class RepeatedListSequence(Sequence):
wrap_end: str = field(default=":)", repr=False) wrap_end: str = field(default=":)", repr=False)
@dataclass @dataclass(kw_only=True)
class Subdivision(Item): class Subdivision(Item):
"""Class for subdivisions""" """Class for subdivisions"""
values: list[Event] values: list[Event]
@dataclass @dataclass(kw_only=True)
class Cyclic(Sequence): class Cyclic(Item):
"""Class for cyclic sequences""" """Class for cyclic sequences"""
values: list
cycle: int = 0 cycle: int = 0
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)
def __next__(self): def __post_init__(self):
yield self.values[self.cycle % len(self.cycle)] super().__post_init__()
self.cycle += 1 self.text = self.__collect_text()
raise StopIteration self.values = [val for val in self.values if not isinstance(val, Whitespace)]
def value(self): def __collect_text(self) -> str:
"""Collect text value from values"""
text = "".join([val.text for val in self.values])
if self.wrap_start is not None:
text = self.wrap_start + text
if self.wrap_end is not None:
text = text + self.wrap_end
return text
def get_value(self, re_eval=False):
"""Get the value for the current cycle""" """Get the value for the current cycle"""
return self.values[self.cycle % len(self.cycle)] value = self.values[self.cycle % len(self.values)]
if re_eval: # If re-evaluated
self.cycle = 0
else:
self.cycle += 1
return value
@dataclass @dataclass(kw_only=True)
class Range(Item): class Range(Item):
"""Class for range""" """Class for range"""
@ -368,35 +568,62 @@ class Range(Item):
end: int = field(default=None) end: int = field(default=None)
ops = { @dataclass(kw_only=True)
"+": operator.add,
"-": operator.sub,
"*": operator.mul,
"/": operator.truediv,
"%": operator.mod,
}
@dataclass
class Operator(Item): class Operator(Item):
"""Class for math operators""" """Class for math operators"""
value: ... = field(init=False, repr=False) value: ...
def __post_init__(self):
self.value = ops[self.text]
@dataclass @dataclass(kw_only=True)
class ListOperation(Sequence): class ListOperation(Sequence):
"""Class for list operations""" """Class for list operations"""
def run(self): def __post_init__(self):
"""Run operations""" super().__post_init__()
pass self.evaluation = True
def filter_operation(self, values):
"""Filtering for the operation elements"""
keep = (Sequence, Event, RandomInteger, Integer, Cyclic)
for item in values:
if isinstance(item, Sequence):
yield item.filter(keep)
elif isinstance(item, keep):
yield item
def evaluate(self, options: dict, re_eval=False):
"""Evaluates the operation"""
operators = self.values[1::2] # Fetch every second operator element
values = self.values[::2] # Fetch every second list element
values = list(self.filter_operation(values)) # Filter out crap
result = values[0] # Start results with the first array
for i, operand in enumerate(operators):
operation = operand.value
right_value = values[i + 1]
if isinstance(right_value, Sequence):
result = [
Pitch(
pitch_class=operation(x.get_value(re_eval), y.get_value(re_eval)),
kwargs=options,
)
for x in result
for y in right_value
]
else:
result = [
Pitch(
pitch_class=operation(
x.get_value(re_eval), right_value.get_value(re_eval)
),
kwargs=options,
)
for x in result
]
return Sequence(values=result)
@dataclass @dataclass(kw_only=True)
class Operation(Item): class Operation(Item):
"""Class for lisp-like operations: (+ 1 2 3) etc.""" """Class for lisp-like operations: (+ 1 2 3) etc."""
@ -404,7 +631,7 @@ class Operation(Item):
operator: operator operator: operator
@dataclass @dataclass(kw_only=True)
class Eval(Sequence): class Eval(Sequence):
"""Class for evaluation notation""" """Class for evaluation notation"""
@ -417,14 +644,14 @@ class Eval(Sequence):
self.result = eval(self.text) self.result = eval(self.text)
@dataclass @dataclass(kw_only=True)
class Atom(Item): class Atom(Item):
"""Class for evaluable atoms""" """Class for evaluable atoms"""
value: ... value: ...
@dataclass @dataclass(kw_only=True)
class Euclid(Item): class Euclid(Item):
"""Class for euclidean cycles""" """Class for euclidean cycles"""
@ -435,7 +662,7 @@ class Euclid(Item):
rotate: int = field(default=None) rotate: int = field(default=None)
@dataclass @dataclass(kw_only=True)
class RepeatedSequence(Sequence): class RepeatedSequence(Sequence):
"""Class for repeats""" """Class for repeats"""

View File

@ -1,4 +1,5 @@
""" Default options for Ziffers """ """ Default options for Ziffers """
import operator
DEFAULT_DURS = { DEFAULT_DURS = {
"m": 8.0, # 15360/1920 "m": 8.0, # 15360/1920
@ -82,6 +83,14 @@ MODIFIERS = {
ROMANS = {"i": 1, "v": 5, "x": 10, "l": 50, "c": 100, "d": 500, "m": 1000} ROMANS = {"i": 1, "v": 5, "x": 10, "l": 50, "c": 100, "d": 500, "m": 1000}
OPERATORS = {
"+": operator.add,
"-": operator.sub,
"*": operator.mul,
"/": operator.truediv,
"%": operator.mod,
}
# pylint: disable=locally-disabled, too-many-lines # pylint: disable=locally-disabled, too-many-lines
SCALES = { SCALES = {

View File

@ -29,7 +29,7 @@ from .classes import (
RepeatedSequence, RepeatedSequence,
) )
from .common import flatten, sum_dict from .common import flatten, sum_dict
from .defaults import DEFAULT_DURS from .defaults import DEFAULT_DURS, OPERATORS
from .scale import parse_roman, chord_from_roman_numeral from .scale import parse_roman, chord_from_roman_numeral
@ -43,8 +43,8 @@ class ZiffersTransformer(Transformer):
def start(self, items) -> Ziffers: def start(self, items) -> Ziffers:
"""Root for the rules""" """Root for the rules"""
seq = Sequence(values=items[0]) # seq = Sequence(values=items[0])
return Ziffers(values=seq, options={}) return Ziffers(values=items[0], options={})
def sequence(self, items): def sequence(self, items):
"""Flatten sequence""" """Flatten sequence"""
@ -53,7 +53,7 @@ class ZiffersTransformer(Transformer):
def random_integer(self, item) -> RandomInteger: def random_integer(self, item) -> RandomInteger:
"""Parses random integer syntax""" """Parses random integer syntax"""
val = item[0][1:-1].split(",") val = item[0][1:-1].split(",")
return RandomInteger(min=val[0], max=val[1], text=item[0].value) return RandomInteger(min=int(val[0]), max=int(val[1]), text=item[0].value)
def range(self, item) -> Range: def range(self, item) -> Range:
"""Parses range syntax""" """Parses range syntax"""
@ -284,14 +284,14 @@ class ZiffersTransformer(Transformer):
) )
return seq return seq
def SIGNED_NUMBER(self, token): def NUMBER(self, token):
"""Parse integer""" """Parse integer"""
val = token.value val = token.value
return Integer(text=val, value=int(val)) return Integer(text=val, value=int(val))
def number(self, item): def number(self, item):
"""Return partial number (Integer or RandomInteger)""" """Return partial number (Integer or RandomInteger)"""
return item return item[0]
def cyclic_number(self, item): def cyclic_number(self, item):
"""Parse cyclic notation""" """Parse cyclic notation"""
@ -310,7 +310,7 @@ class ZiffersTransformer(Transformer):
def operator(self, token): def operator(self, token):
"""Parse operator""" """Parse operator"""
val = token[0].value val = token[0].value
return Operator(text=val) return Operator(text=val, value=OPERATORS[val])
def list_items(self, items): def list_items(self, items):
"""Parse sequence""" """Parse sequence"""
@ -320,6 +320,10 @@ class ZiffersTransformer(Transformer):
"""Parse list operation""" """Parse list operation"""
return ListOperation(values=items) return ListOperation(values=items)
def right_op(self,items):
"""Get right value for the operation"""
return items[0]
def euclid(self, items): def euclid(self, items):
"""Parse euclid notation""" """Parse euclid notation"""
params = items[1][1:-1].split(",") params = items[1][1:-1].split(",")

View File

@ -41,58 +41,14 @@ def zparse(expr: str, **opts) -> Ziffers:
""" """
parsed = parse_expression(expr) parsed = parse_expression(expr)
if opts: if opts:
parsed.set_defaults(opts) parsed.init_opts(opts)
return parsed return parsed
# pylint: disable=invalid-name # pylint: disable=invalid-name
def z0(expr: str, **opts) -> Ziffers: def z(expr: str, **opts) -> Ziffers:
"""Shortened method name for zparse"""
return zparse(expr, **opts)
def z1(expr: str, **opts) -> Ziffers:
"""Shortened method name for zparse"""
return zparse(expr, **opts)
def z2(expr: str, **opts) -> Ziffers:
"""Shortened method name for zparse"""
return zparse(expr, **opts)
def z3(expr: str, **opts) -> Ziffers:
"""Shortened method name for zparse"""
return zparse(expr, **opts)
def z4(expr: str, **opts) -> Ziffers:
"""Shortened method name for zparse"""
return zparse(expr, **opts)
def z5(expr: str, **opts) -> Ziffers:
"""Shortened method name for zparse"""
return zparse(expr, **opts)
def z6(expr: str, **opts) -> Ziffers:
"""Shortened method name for zparse"""
return zparse(expr, **opts)
def z7(expr: str, **opts) -> Ziffers:
"""Shortened method name for zparse"""
return zparse(expr, **opts)
def z8(expr: str, **opts) -> Ziffers:
"""Shortened method name for zparse"""
return zparse(expr, **opts)
def z9(expr: str, **opts) -> Ziffers:
"""Shortened method name for zparse""" """Shortened method name for zparse"""
return zparse(expr, **opts) return zparse(expr, **opts)

View File

@ -209,7 +209,7 @@ def midi_to_pitch_class(note: int, key: str | int, scale: str) -> dict:
scale (str): Used scale scale (str): Used scale
Returns: Returns:
tuple: Returns tuple containing (pitch class as string, pitch class, octave, optional modifier) tuple: Returns dict containing pitch-class values
""" """
pitch_class = note % 12 pitch_class = note % 12
octave = midi_to_octave(note) - 5 octave = midi_to_octave(note) - 5

View File

@ -18,7 +18,7 @@
?roman_number: /iv|v|v?i{1,3}/ ?roman_number: /iv|v|v?i{1,3}/
// Valid as integer // Valid as integer
?number: NUMBER | random_integer | cyclic_number number: NUMBER | random_integer | cyclic_number
cyclic_number: "<" number (WS number)* ">" cyclic_number: "<" number (WS number)* ">"
// Repeats // Repeats
@ -29,7 +29,8 @@
repeated_list: prefix* "(:" sequence ":" [number] ")" repeated_list: prefix* "(:" sequence ":" [number] ")"
// Right recursive list operation // Right recursive list operation
list_op: list (operator (list | number))+ list_op: list (operator right_op)+
right_op: list | number
operator: /([\+\-\*\/%]|<<|>>)/ operator: /([\+\-\*\/%]|<<|>>)/
// Euclidean cycles // Euclidean cycles
@ -64,8 +65,8 @@
random_integer: /\(-?[0-9]+,-?[0-9]+\)/ random_integer: /\(-?[0-9]+,-?[0-9]+\)/
range: /-?[0-9]\.\.-?[0-9]/ range: /-?[0-9]\.\.-?[0-9]/
cycle: "<" sequence ">" cycle: "<" sequence ">"
random_pitch: "?" random_pitch: /(\?)(?!\d)/
random_percent: "%" random_percent: /(%)(?!\d)/
// Rules for evaluating clauses inside {} // Rules for evaluating clauses inside {}
// TODO: Support for parenthesis? // TODO: Support for parenthesis?