""" Lark transformer for mapping Lark tokens to Ziffers objects """ import random from lark import Transformer, Token from .scale import cents_to_semitones, ratio_to_cents, monzo_to_cents from .classes.root import Ziffers from .classes.sequences import ( Sequence, ListSequence, RepeatedListSequence, ListOperation, RepeatedSequence, Euclid, Subdivision, Eval, Operation, LispOperation, ) from .classes.items import ( Whitespace, DurationChange, OctaveChange, OctaveAdd, Pitch, Rest, RandomPitch, RandomPercent, Chord, RomanNumeral, Cyclic, RandomInteger, Range, Operator, Atom, Integer, VariableAssignment, Variable, VariableList, Measure, ) from .common import flatten, sum_dict from .defaults import DEFAULT_DURS, OPERATORS from .scale import parse_roman # pylint: disable=locally-disabled, unused-argument, too-many-public-methods, invalid-name class ZiffersTransformer(Transformer): """Rules for transforming Ziffers expressions into tree.""" def start(self, items) -> Ziffers: """Root for the rules""" return Ziffers(values=items[0], options={}) def sequence(self, items): """Flatten sequence""" return flatten(items) def rest(self, items): """Return duration event""" if len(items) > 0: prefixes = sum_dict(items) text_prefix = prefixes.pop("text") prefixes["prefix"] = text_prefix return Rest(text=text_prefix + "r", local_options=prefixes) return Rest(text="r") def measure(self, items): """Return new measure""" return Measure() 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 random_integer_re(self, items): """Return random integer notation from regex""" return items[0].value def range(self, items) -> Range: """Parses range 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].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: """Parses cycle""" values = items[0] return Cyclic(values=values) def pitch_class(self, items): """Parses pitch class""" # If there are prefixes if len(items) > 1: # Collect&sum prefixes from any order: _qee^s4 etc. prefixes = sum_dict(items[0:-1]) # If there are prefixes text_prefix = prefixes.pop("text") prefixes["prefix"] = text_prefix pitch = Pitch( pitch_class=items[-1]["pitch_class"], text=text_prefix + items[-1]["text"], local_options=prefixes, ) return pitch val = items[0] return Pitch(**val) def pitch(self, items): """Return pitch class info""" text_value = items[0].value.replace("T", "10").replace("E", "11") return {"pitch_class": int(text_value), "text": items[0].value} def escaped_pitch(self, items): """Return escaped pitch""" val = items[0].value[1:-1] return {"pitch_class": int(val), "text": val} def prefix(self, items): """Return prefix""" return items[0] def oct_change(self, items): """Parses octave change""" octave = items[0] return [ OctaveChange(value=octave["octave_change"], text=octave["text"]), items[1], ] def oct_mod(self, items): """Parses octave modification""" octave = items[0] return [OctaveAdd(value=octave["octave"], text=octave["text"]), items[1]] def escaped_octave(self, items): """Return octave info""" value = items[0][1:-1] return {"octave_change": int(value), "text": items[0].value} def octave(self, items): """Return octaves ^ and _""" value = sum(1 if char == "^" else -1 for char in items[0].value) return {"octave": value, "text": items[0].value} def modifier(self, items): """Return modifiers # and b""" value = 1 if items[0].value == "#" else -1 return {"modifier": value, "text": items[0].value} def chord(self, items): """Parses chord""" if isinstance(items[-1], Token): return Chord( pitch_classes=items[0:-1], text="".join([val.text for val in items[0:-1]]), inversions=int(items[-1].value[1:]), ) return Chord(pitch_classes=items, text="".join([val.text for val in items])) def invert(self, items): """Return chord inversion""" return items[0] def named_roman(self, items) -> RomanNumeral: """Parse chord from roman numeral""" numeral = items[0].value # TODO: Refactor this and the rule if len(items) == 1: return RomanNumeral(value=parse_roman(numeral), text=numeral) if len(items) > 2: name = items[1] inversions = int(items[-1].value[1:]) return RomanNumeral( text=numeral, value=parse_roman(numeral), chord_type=name, inversions=inversions, ) elif len(items) == 2: if isinstance(items[-1], Token): inversions = int(items[-1].value[1:]) return RomanNumeral( value=parse_roman(numeral), text=numeral, inversions=inversions, ) else: return RomanNumeral( value=parse_roman(numeral), text=numeral, chord_type=items[1] ) def chord_name(self, item): """Return name for chord""" return item[0].value def roman_number(self, item): """Return roman numeral""" return item[0] def dur_change(self, items): """Parses duration change""" durs = items[0] return DurationChange(value=durs["duration"], text=durs["text"]) def char_change(self, items): """Return partial duration char info""" chars = "" durs = 0.0 for dchar, dots in items: val = DEFAULT_DURS[dchar] if dots > 0: val = val * (2.0 - (1.0 / (2 * dots))) chars = chars + (dchar + "." * dots) durs = durs + val return {"text": chars, "duration": durs} def dchar_not_prefix(self, items): """Return partial duration char info""" dur = items[0].split(".", 1) dots = 0 if len(dur) > 1: dots = len(dur[1]) + 1 return [dur[0], dots] def escaped_decimal(self, items): """Return partial decimal info""" val = items[0] val["text"] = "<" + val["text"] + ">" return val def random_pitch(self, items): """Parses random pitch""" return RandomPitch(text="?") def random_percent(self, items): """Parses random percent""" return RandomPercent(text="%") def duration_chars(self, items): """Return partial duration info""" durations = [val[1] for val in items] characters = "".join([val[0] for val in items]) return {"duration": sum(durations), "text": characters} def dotted_dur(self, items): """Return partial duration info""" key = items[0] val = DEFAULT_DURS[key] dots = len(items) - 1 if dots > 0: val = val * (2.0 - (1.0 / (2 * dots))) return [key + "." * dots, val] def decimal(self, items): """Return partial duration info""" val = items[0] return {"duration": float(val), "text": val.value} def dot(self, items): """Return partial duration info""" return "." def dchar(self, items): """Return partial duration info""" chardur = items[0].value return chardur def WS(self, items): """Parse whitespace""" return Whitespace(text=items[0]) def subdivision(self, items): """Parse subdivision""" values = flatten(items[0]) return Subdivision(values=values, wrap_start="[", wrap_end="]") def subitems(self, items): """Return subdivision items""" return items # Eval rules def eval(self, items): """Parse eval""" return Eval(values=items) def sub_operations(self, items): """Returns list of operations""" return Eval(values=items[0], wrap_start="(", wrap_end=")") def operation(self, items): """Return partial eval operations""" if isinstance(items[0], dict): local_opts = items[0] del local_opts["text"] return Operation(values=flatten(items[1:]), local_options=items[0]) return Operation(values=flatten(items)) def atom(self, token): """Return partial eval item""" val = token[0].value return Atom(value=val, text=str(val)) # Variable assignment def assignment(self, items): """Creates variable assignment""" var = items[0] op = items[1] content = items[2] return VariableAssignment( variable=var, value=content, text=var.text + "=" + content.text, pre_eval=True if op == "=" else False, ) def ass_op(self, items): """Return parsed type for assignment: = or ~""" return items[0].value def variable(self, items): """Return variable""" if len(items) > 1: prefixes = sum_dict(items[0:-1]) text_prefix = prefixes.pop("text") return Variable( name=items[-1], text=text_prefix + items[-1], local_options=prefixes ) return Variable(name=items[0], text=items[0]) def variable_char(self, items): """Return parsed variable name""" return items[0].value # Variable(name=items[0].value, text=items[0].value) def variablelist(self, items): """Return list of variables""" return VariableList(values=items, text="".join([item.text for item in items])) # List rules def list(self, items): """Parse list sequence notation, ex: (1 2 3)""" if len(items) > 1: prefixes = sum_dict(items[0:-1]) text_prefix = prefixes.pop("text") prefixes["prefix"] = text_prefix values = items[-1] seq = ListSequence( values=values, wrap_start=prefixes["prefix"] + "(", local_options=prefixes, ) return seq else: seq = ListSequence(values=items[0]) return seq def repeated_list(self, items): """Parse repeated list notation ex: (: 1 2 3 :)""" if len(items) > 2: prefixes = sum_dict(items[0:-2]) # If there are prefixes if items[-1] is not None: seq = RepeatedListSequence( values=items[-2], repeats=items[-1], wrap_end=":" + items[-1].text + ")", local_options=prefixes, ) else: seq = RepeatedListSequence( values=items[-2], repeats=Integer(text="2", value=2, local_options=prefixes), ) return seq else: if items[-1] is not None: seq = RepeatedListSequence( values=items[-2], repeats=items[-1], wrap_end=":" + items[-1].text + ")", ) else: seq = RepeatedListSequence( values=items[-2], repeats=Integer(text="2", value=2) ) return seq def integer(self, items): """Parses integer from single ints""" concatted = sum_dict(items) val = concatted["text"] return Integer(text=val, value=int(val)) def number(self, item): """Return partial number (Integer or RandomInteger)""" return item[0] def cyclic_number(self, item): """Parse cyclic notation""" return Cyclic(values=item) def lisp_operation(self, items): """Parse lisp like list operation""" op = items[0] values = items[2] return LispOperation( operator=op, values=values, text="(+" + "".join([v.text for v in values]) + ")", ) def operator(self, token): """Parse operator""" val = token[0].value return Operator(text=val, value=OPERATORS[val]) def list_operator(self, token): """Parse list operator""" val = token[0].value return Operator(text=val, value=OPERATORS[val]) def list_items(self, items): """Parse sequence""" return Sequence(values=items) def list_op(self, items): """Parse list operation""" return ListOperation(values=items) def right_op(self, items): """Get right value for the operation""" return items[0] def euclid(self, items): """Parse euclid notation""" params = items[1][1:-1].split(",") init = {"onset": items[0], "pulses": int(params[0]), "length": int(params[1])} text = items[0].text + items[1] if len(params) > 2: init["rotate"] = int(params[2]) if len(items) > 2: init["offset"] = items[2] text = text + items[2].text init["text"] = text return Euclid(**init) def euclid_operator(self, token): """Return euclid operators""" return token.value def repeat(self, items): """Parse repeated sequence, ex: [: 1 2 3 :]""" if items[-1] is not None: return RepeatedSequence( values=items[0], repeats=items[-1], wrap_end=":" + items[-1].text + "]" ) else: return RepeatedSequence(values=items[0], repeats=Integer(value=2, text="2")) def repeat_item(self, items): """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, ) # pylint: disable=locally-disabled, unused-argument, too-many-public-methods, invalid-name, eval-used class ScalaTransformer(Transformer): """Transformer for scala scales""" def lines(self, items): """Transforms cents to semitones""" cents = [ ratio_to_cents(item) if isinstance(item, int) else item for item in items ] return cents_to_semitones(cents) def operation(self, items): """Get operation""" # Safe eval. Items are pre-parsed. val = eval("".join(str(item) for item in items)) return val def operator(self, items): """Get operator""" return items[0].value def sub_operations(self, items): """Get sub-operation""" return "(" + items[0] + ")" def frac_ratio(self, items): """Get ration as fraction""" ratio = items[0] / items[1] return ratio_to_cents(ratio) def decimal_ratio(self, items): """Get ratio as decimal""" ratio = float(str(items[0]) + "." + str(items[1])) return ratio_to_cents(ratio) def monzo(self, items): """Get monzo ratio""" return monzo_to_cents(items) def edo_ratio(self, items): """Get EDO ratio""" ratio = pow(2, items[0] / items[1]) return ratio_to_cents(ratio) def edji_ratio(self, items): """Get EDJI ratio""" if len(items) > 3: power = items[2] / items[3] else: power = items[2] ratio = pow(power, items[0] / items[1]) return ratio_to_cents(ratio) def int(self, items): """Get integer""" return int(items[0].value) def float(self, items): """Get float""" return float(items[0].value) def random_int(self, items): """Get random integer""" def _rand_between(start, end): return random.randint(min(start, end), max(start, end)) start = items[0] end = items[1] rand_val = _rand_between(start, end) return rand_val def random_decimal(self, items): """Get random decimal""" def _rand_between(start, end): return random.uniform(min(start, end), max(start, end)) start = items[0] end = items[1] rand_val = _rand_between(start, end) return rand_val