diff --git a/tests/test_multi_03.py b/tests/test_multi_03.py index fd6bda1..55465f4 100644 --- a/tests/test_multi_03.py +++ b/tests/test_multi_03.py @@ -7,6 +7,7 @@ from ziffers import zparse [ ("1 2 3", [[1, 2, 3], [0.25,0.25,0.25]]), ("q2 eq3 e.4", [[2, 3, 4], [0.25,0.375,0.1875]]), + ("{q9 e10 23}", [[9,10,23],[0.25,0.125,0.25]]) ], ) def test_multi_var(pattern: str, expected: list): diff --git a/ziffers/classes/__init__.py b/ziffers/classes/__init__.py index e5a65ad..d496915 100644 --- a/ziffers/classes/__init__.py +++ b/ziffers/classes/__init__.py @@ -1,3 +1,3 @@ from .items import * from .root import * -from .sequences import * \ No newline at end of file +from .sequences import * diff --git a/ziffers/classes/items.py b/ziffers/classes/items.py index b4478f3..c3ada80 100644 --- a/ziffers/classes/items.py +++ b/ziffers/classes/items.py @@ -1,7 +1,6 @@ """ Ziffers item classes """ from dataclasses import dataclass, field, asdict from math import floor -import operator import random from ..scale import ( note_from_pc, @@ -149,6 +148,26 @@ class Event(Item): class Rest(Event): """Class for rests""" + def get_note(self): + """Getter for note""" + return None + + def get_freq(self): + """Getter for freq""" + return None + + def get_octave(self): + """Getter for octave""" + return None + + def get_pitch_class(self): + """Getter for pitche""" + return None + + def get_pitch_bend(self): + """Getter for pitche""" + return None + @dataclass class Measure(Item): @@ -283,9 +302,16 @@ class RandomPitch(Event): Returns: int: Returns random pitch """ - return random.randint( - 0, get_scale_length(options.get("scale", "Major")) if options else 9 - ) + if options: + scale = options["scale"] + if isinstance(scale, str): + scale_length = get_scale_length(options.get("scale", "Major")) + else: + scale_length = len(scale) + else: + scale_length = 9 + + return random.randint(0, scale_length) @dataclass(kw_only=True) @@ -604,14 +630,6 @@ class Operator(Item): value: ... -@dataclass(kw_only=True) -class Operation(Item): - """Class for lisp-like operations: (+ 1 2 3) etc.""" - - values: list - operator: operator - - @dataclass(kw_only=True) class Atom(Item): """Class for evaluable atoms""" diff --git a/ziffers/classes/root.py b/ziffers/classes/root.py index 5529f28..165914a 100644 --- a/ziffers/classes/root.py +++ b/ziffers/classes/root.py @@ -3,7 +3,7 @@ from dataclasses import dataclass, field from itertools import islice, cycle from ..defaults import DEFAULT_OPTIONS -from .items import Item, Pitch, Chord, Event +from .items import Item, Pitch, Chord, Event, Rest from .sequences import Sequence, Subdivision @@ -32,7 +32,7 @@ class Ziffers(Sequence): def __len__(self): return len(self.evaluated_values) - + def __iter__(self): return self @@ -107,15 +107,15 @@ class Ziffers(Sequence): return [ val.get_pitch_class() for val in self.evaluated_values - if isinstance(val, (Pitch, Chord)) + if isinstance(val, (Pitch, Chord, Rest)) ] - + def pitch_bends(self) -> list[int]: """Return list of pitch bend values""" return [ val.get_pitch_bend() for val in self.evaluated_values - if isinstance(val, (Pitch, Chord)) + if isinstance(val, (Pitch, Chord, Rest)) ] def notes(self) -> list[int]: @@ -123,7 +123,7 @@ class Ziffers(Sequence): return [ val.get_note() for val in self.evaluated_values - if isinstance(val, (Pitch, Chord)) + if isinstance(val, (Pitch, Chord, Rest)) ] def durations(self) -> list[float]: @@ -133,10 +133,12 @@ class Ziffers(Sequence): for val in self.evaluated_values if isinstance(val, Event) ] - + def total_duration(self) -> float: """Return total duration""" - return sum([val.duration for val in self.evaluated_values if isinstance(val, Event)]) + return sum( + [val.duration for val in self.evaluated_values if isinstance(val, Event)] + ) def total_beats(self) -> float: """Return total beats""" @@ -161,7 +163,7 @@ class Ziffers(Sequence): return [ val.get_octave() for val in self.evaluated_values - if isinstance(val, (Pitch, Chord)) + if isinstance(val, (Pitch, Chord, Rest)) ] def freqs(self) -> list[int]: @@ -169,9 +171,9 @@ class Ziffers(Sequence): return [ val.get_freq() for val in self.evaluated_values - if isinstance(val, (Pitch, Chord)) + if isinstance(val, (Pitch, Chord, Rest)) ] - + def collect(self, num: int = None, keys: str | list = None) -> list: """Collect n items from parsed Ziffers""" if num is None: diff --git a/ziffers/classes/sequences.py b/ziffers/classes/sequences.py index 2c42167..5b66b17 100644 --- a/ziffers/classes/sequences.py +++ b/ziffers/classes/sequences.py @@ -4,8 +4,9 @@ from itertools import product from math import floor from types import LambdaType from copy import deepcopy +import operator from ..defaults import DEFAULT_OPTIONS -from ..common import cyclic_zip, euclidian_rhythm +from ..common import cyclic_zip, euclidian_rhythm, flatten from ..scale import note_from_pc, midi_to_freq from .items import ( Meta, @@ -31,7 +32,7 @@ from .items import ( Modification, Whitespace, Sample, - SampleList + SampleList, ) @@ -46,6 +47,8 @@ def resolve_item(item: Meta, options: dict): elif isinstance(item, Subdivision): item.evaluate_values(options) yield item + elif isinstance(item, Eval): + yield from item.evaluate_values(options) else: yield from item.evaluate_tree(options) elif isinstance(item, VariableAssignment): @@ -66,14 +69,14 @@ def resolve_item(item: Meta, options: dict): run=opt_item, text=item.text, kwargs=(options | item.local_options), - local_options=item.local_options + local_options=item.local_options, ) elif isinstance(opt_item, str): yield Sample( name=opt_item, text=item.text, kwargs=(options | item.local_options), - local_options=item.local_options + local_options=item.local_options, ) variable = deepcopy(opt_item) yield from resolve_item(variable, options) @@ -90,7 +93,7 @@ def resolve_item(item: Meta, options: dict): run=opt_item, text=var.text, kwargs=(options | var.local_options), - local_options=var.local_options + local_options=var.local_options, ) ) elif isinstance(opt_item, str): @@ -99,7 +102,7 @@ def resolve_item(item: Meta, options: dict): name=opt_item, text=var.text, kwargs=(options | var.local_options), - local_options=var.local_options + local_options=var.local_options, ) ) elif isinstance(opt_item, Sequence): @@ -343,7 +346,7 @@ class Subdivision(Sequence): if isinstance(item, Event): if duration is not None: item.duration = new_d - item.beat = new_d*4 + item.beat = new_d * 4 yield item @@ -373,6 +376,8 @@ class ListOperation(Sequence): flattened_list.extend(list(item.evaluate_durations())) elif isinstance(item, RepeatedListSequence): flattened_list.extend(list(item.resolve_repeat(options))) + elif isinstance(item, Eval): + flattened_list.extend(item.evaluate_values(options)) else: flattened_list.append(_filter_operation(item, options)) elif isinstance(item, Cyclic): @@ -543,13 +548,43 @@ class ListOperation(Sequence): class Eval(Sequence): """Class for evaluation notation""" - result: ... = None wrap_start: str = field(default="{", repr=False) wrap_end: str = field(default="}", repr=False) - def __post_init__(self): - super().__post_init__() - self.result = eval(self.text) + # def __post_init__(self): + # self.text = "".join([val.text for val in flatten(self.values)]) + # super().__post_init__() + + def evaluate_values(self, options): + operations = [val for val in self.values if isinstance(val, (Operation, Rest))] + eval_values = [] + for val in operations: + if isinstance(val,Operation): + eval_values.append(Pitch(pitch_class=val.evaluate(), kwargs=options | val.local_options)) + else: + eval_values.append(val) + + self.evaluated_values = eval_values + + return self.evaluated_values + + +@dataclass(kw_only=True) +class LispOperation(Sequence): + """Class for lisp-like operations: (+ 1 2 3) etc.""" + + values: list + operator: operator + + +@dataclass(kw_only=True) +class Operation(Sequence): + """Class for sequential operations""" + + values: list + + def evaluate(self): + return eval(self.text) @dataclass(kw_only=True) @@ -588,6 +623,8 @@ class RepeatedSequence(Sequence): elif isinstance(item, Subdivision): item.evaluate_values(options) yield item + elif isinstance(item, Eval): + yield from item.evaluate_values(options) else: yield from item elif isinstance(item, Cyclic): diff --git a/ziffers/common.py b/ziffers/common.py index 285828f..17fe4e3 100644 --- a/ziffers/common.py +++ b/ziffers/common.py @@ -2,6 +2,7 @@ import re from copy import deepcopy + def flatten(arr: list) -> list: """Flattens array""" return ( @@ -23,14 +24,16 @@ def rotate(arr, k): arr = arr[-k:] + arr[:-k] return arr -def repeat_text(pos,neg,times): + +def repeat_text(pos, neg, times): """Helper to repeat text""" - if times>0: - return pos*times - if times<0: - return neg*abs(times) + if times > 0: + return pos * times + if times < 0: + return neg * abs(times) return "" + def sum_dict(arr: list[dict]) -> dict: """Sums a list of dicts: [{a:3,b:3},{b:1}] -> {a:3,b:4}""" result = arr[0] @@ -110,4 +113,4 @@ def cyclic_zip(first: list, second: list) -> list: f_length = len(first) for i in range(s_length): result.append([first[i % f_length], second[i]]) - return [deepcopy(item) for sublist in result for item in sublist] \ No newline at end of file + return [deepcopy(item) for sublist in result for item in sublist] diff --git a/ziffers/generators.py b/ziffers/generators.py new file mode 100644 index 0000000..e09bf44 --- /dev/null +++ b/ziffers/generators.py @@ -0,0 +1,35 @@ +"""Collection of generators""" + + +# Sieve of Eratosthenes +# Based on code by David Eppstein, UC Irvine, 28 Feb 2002 +# http://code.activestate.com/recipes/117119/ +def gen_primes(): + """Generate an infinite sequence of prime numbers.""" + # Maps composites to primes witnessing their compositeness. + # This is memory efficient, as the sieve is not "run forward" + # indefinitely, but only as long as required by the current + # number being tested. + sieve = {} + + # The running integer that's checked for primeness + current = 2 + + while True: + if current not in sieve: + # current is a new prime. + # Yield it and mark its first multiple that isn't + # already marked in previous iterations + yield current + sieve[current * current] = [current] + else: + # current is composite. sieve[current] is the list of primes that + # divide it. Since we've reached current, we no longer + # need it in the map, but we'll mark the next + # multiples of its witnesses to prepare for larger + # numbers + for composite in sieve[current]: + sieve.setdefault(composite + current, []).append(composite) + del sieve[current] + + current += 1 diff --git a/ziffers/mapper.py b/ziffers/mapper.py index c949709..0443838 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -1,8 +1,7 @@ """ Lark transformer for mapping Lark tokens to Ziffers objects """ import random -from math import log, pow from lark import Transformer, Token -from .scale import cents_to_semitones, ratio_to_cents +from .scale import cents_to_semitones, ratio_to_cents, monzo_to_cents from .classes.root import Ziffers from .classes.sequences import ( Sequence, @@ -13,6 +12,8 @@ from .classes.sequences import ( Euclid, Subdivision, Eval, + Operation, + LispOperation, ) from .classes.items import ( Whitespace, @@ -29,7 +30,6 @@ from .classes.items import ( RandomInteger, Range, Operator, - Operation, Atom, Integer, VariableAssignment, @@ -138,6 +138,11 @@ class ZiffersTransformer(Transformer): 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] @@ -305,8 +310,7 @@ class ZiffersTransformer(Transformer): def eval(self, items): """Parse eval""" - val = items[0] - return Eval(values=val) + return Eval(values=items) def sub_operations(self, items): """Returns list of operations""" @@ -314,12 +318,16 @@ class ZiffersTransformer(Transformer): def operation(self, items): """Return partial eval operations""" - return flatten(items) + 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=val) + return Atom(value=val, text=str(val)) # Variable assignment @@ -340,6 +348,7 @@ class ZiffersTransformer(Transformer): 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") @@ -405,9 +414,10 @@ class ZiffersTransformer(Transformer): ) return seq - def NUMBER(self, token): - """Parse integer""" - val = token.value + 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): @@ -422,7 +432,7 @@ class ZiffersTransformer(Transformer): """Parse lisp like list operation""" op = items[0] values = items[1:] - return Operation( + return LispOperation( operator=op, values=values, text="(+" + "".join([v.text for v in values]) + ")", @@ -433,6 +443,11 @@ class ZiffersTransformer(Transformer): 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) @@ -483,44 +498,67 @@ class ZiffersTransformer(Transformer): # 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): - cents = [ratio_to_cents(item) if isinstance(item,int) else item for item in 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 ratio(self, items): - ratio = items[0]/items[1] + 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): - ratio = pow(2,items[0]/items[1]) + """Get EDO ratio""" + ratio = pow(2, items[0] / items[1]) return ratio_to_cents(ratio) - + def edji_ratio(self, items): - if len(items)>3: - power = items[2]/items[3] + """Get EDJI ratio""" + if len(items) > 3: + power = items[2] / items[3] else: power = items[2] - ratio = pow(power,items[0]/items[1]) + 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)) @@ -529,8 +567,9 @@ class ScalaTransformer(Transformer): 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)) diff --git a/ziffers/scale.py b/ziffers/scale.py index 5bf359c..3ef1836 100644 --- a/ziffers/scale.py +++ b/ziffers/scale.py @@ -2,7 +2,9 @@ #!/usr/bin/env python3 # pylint: disable=locally-disabled, no-name-in-module import re -from math import log2, floor, log +from math import log2 +from itertools import islice +from .generators import gen_primes from .common import repeat_text from .defaults import ( SCALES, @@ -70,7 +72,7 @@ def note_name_to_midi(name: str) -> int: return 12 + octave * 12 + interval + modifier -def get_scale(name: str) -> list[int]: +def get_scale(scale: str) -> list[int]: """Get a scale from the global scale list Args: @@ -79,7 +81,10 @@ def get_scale(name: str) -> list[int]: Returns: list: List of intervals in the scale """ - scale = SCALES.get(name.lower().capitalize(), SCALES["Ionian"]) + if isinstance(scale, (list, tuple)): + return scale + + scale = SCALES.get(scale.lower().capitalize(), SCALES["Ionian"]) return scale @@ -102,7 +107,11 @@ def get_scale_notes(name: str, root: int = 60, num_octaves: int = 1) -> list[int def get_chord_from_scale( - degree: int, root: int = 60, scale: str = "Major", num_notes: int = 3, skip: int = 2 + degree: int, + root: int = 60, + scale: str | tuple = "Major", + num_notes: int = 3, + skip: int = 2, ) -> list[int]: """Generate chord from the scale by skipping notes @@ -116,12 +125,17 @@ def get_chord_from_scale( Returns: list[int]: List of midi notes """ - num_of_octaves = ((num_notes * skip + degree) // get_scale_length(scale)) + 1 + if isinstance(scale, str): + scale_length = get_scale_length(scale) + else: + scale_length = len(scale) + + num_of_octaves = ((num_notes * skip + degree) // scale_length) + 1 scale_notes = get_scale_notes(scale, root, num_of_octaves) return scale_notes[degree - 1 :: skip][:num_notes] -def get_scale_length(name: str) -> int: +def get_scale_length(scale: str) -> int: """Get length of the scale Args: @@ -130,8 +144,10 @@ def get_scale_length(name: str) -> int: Returns: int: Length of the scale """ - scale = SCALES.get(name.lower().capitalize(), SCALES["Ionian"]) - return len(scale) + if isinstance(scale, (list, tuple)): + return len(scale) + + return len(SCALES.get(scale.lower().capitalize(), SCALES["Ionian"])) # pylint: disable=locally-disabled, too-many-arguments @@ -271,9 +287,9 @@ def midi_to_pitch_class(note: int, key: str | int, scale: str) -> dict: Returns: tuple: Returns dict containing pitch-class values """ - pitch_class = note % 12 + pitch_class = int(note % 12) # Cast to int "fixes" microtonal scales octave = midi_to_octave(note) - 5 - if scale.upper() == "CHROMATIC": + if isinstance(scale, str) and scale.upper() == "CHROMATIC": return {"text": str(pitch_class), "pitch_class": pitch_class, "octave": octave} sharps = ["0", "#0", "1", "#1", "2", "3", "#3", "4", "#4", "5", "#5", "6"] @@ -319,7 +335,11 @@ def chord_from_degree( """ root = note_name_to_midi(root) if isinstance(root, str) else root - if name is None and scale.lower().capitalize() == "Chromatic": + if ( + name is None + and isinstance(scale, str) + and scale.lower().capitalize() == "Chromatic" + ): name = "major" if name: @@ -387,6 +407,32 @@ def cents_to_semitones(cents: list) -> tuple[float]: semitone_scale.append(semitone_interval) return tuple(semitone_scale) + def ratio_to_cents(ratio: float) -> float: """Transform ratio to cents""" - return 1200.0 * log(float(ratio), 2) + return 1200.0 * log2(float(ratio)) + + +def monzo_to_cents(monzo) -> float: + """ + Convert a monzo to cents using the prime factorization method. + + Args: + monzo (list): A list of integers representing the exponents of the prime factorization + + Returns: + float: The value in cents + """ + # Calculate the prime factors of the indices in the monzo + max_index = len(monzo) + primes = list(islice(gen_primes(), max_index + 1)) + + # Product of the prime factors raised to the corresponding exponents + ratio = 1 + for i in range(max_index): + ratio *= primes[i] ** monzo[i] + + # Frequency ratio to cents + cents = 1200 * log2(ratio) + + return cents diff --git a/ziffers/spec/scala.lark b/ziffers/spec/scala.lark index a42dba6..fddc894 100644 --- a/ziffers/spec/scala.lark +++ b/ziffers/spec/scala.lark @@ -1,20 +1,24 @@ ?root: lines -lines: (number | operation | ratio | edo_ratio | edji_ratio)+ +lines: (number | ratio | monzo | operation)+ -operation: (number | ratio) (operator ((number | ratio) | sub_operations | operation))+ -ratio: (int | random_int) "/" (int | random_int) -edo_ratio: (int | random_int) "\\" (int | random_int) -edji_ratio: (int | random_int) "\\" (int | random_int) "<" (int | random_int) "/"? (int | random_int)? ">" -!operator: "+" | "-" | "*" | "%" | "&" | "|" | "<<" | ">>" -sub_operations: "(" operation ")" - -// Signed number without EXP ?number: float | int | random_int | random_float random_int: "(" int "," int ")" random_float: "(" float "," float ")" float: /(-?[0-9]+\.[0-9]*)|(\.[0-9]+)/ -int: /[0-9]+/ +int: /-?[0-9]+/ + +?ratio: frac_ratio | edo_ratio | edji_ratio | decimal_ratio +frac_ratio: (int | random_int) "/" (int | random_int) +edo_ratio: (int | random_int) "\\" (int | random_int) +edji_ratio: (int | random_int) "\\" (int | random_int) "<" (int | random_int) "/"? (int | random_int)? ">" +decimal_ratio: int "," int + +monzo: "[" int+ ">" + +operation: (number | ratio | monzo) (operator ((number | ratio | monzo) | sub_operations | operation))+ +!operator: "+" | "-" | "*" | "%" | "&" | "|" | "<<" | ">>" +sub_operations: "(" operation ")" %import common.WS %ignore WS \ No newline at end of file diff --git a/ziffers/spec/ziffers.lark b/ziffers/spec/ziffers.lark index 0066d26..03466b9 100644 --- a/ziffers/spec/ziffers.lark +++ b/ziffers/spec/ziffers.lark @@ -3,7 +3,7 @@ sequence: (pitch_class | repeat_item | assignment | variable | variablelist | rest | dur_change | oct_mod | oct_change | WS | measure | chord | named_roman | cycle | random_integer | random_pitch | random_percent | range | list | repeated_list | lisp_operation | list_op | subdivision | eval | euclid | repeat)* // Pitch classes - pitch_class: prefix* pitch + pitch_class: prefix* (pitch | escaped_pitch) prefix: (octave | duration_chars | escaped_decimal | escaped_octave | modifier) pitch: /-?[0-9TE]/ escaped_decimal: "<" decimal ">" @@ -39,10 +39,9 @@ invert: /%-?[0-9][0-9]*/ // Valid as integer - number: NUMBER | random_integer | cycle - - // CYCLIC NUMBERS NOT IN USE. NUMBERS MUST BE VALIDATED FROM FULL CYCLES! - // cyclic_number: "<" number (WS number)* ">" + number: integer | random_integer | cycle + integer: pitch+ + escaped_pitch: /{-?[0-9]+}/ // Repeats repeat: "[:" sequence ":" [number] "]" @@ -53,10 +52,12 @@ repeated_list: prefix* "(:" sequence ":" [number] ")" // Right recursive list operation - list_op: list (operator right_op)+ + list_op: list ((operator | list_operator) right_op)+ right_op: list | number - //operator: "+" | "-" | "*" | "%" | "&" | "|" | "<<" | ">>" | "@" | "#" - operator: /([\+\-\*\/%\|\&]|<<|>>|@|#|<>)/ + // Operators that work only on lists: | << >> + list_operator: /(\||<<|>>|<>|#|@)(?=[(\d])/ + // Common operators that works with numbers 3+5 3-5 etc. + operator: /([\+\-\*\/%\&])/ // Euclidean cycles // TODO: Support randomization etc. @@ -88,13 +89,9 @@ random_percent: /(%)(?!\d)/ // Rules for evaluating clauses inside {} - // TODO: Support for parenthesis? - eval: "{" operation+ "}" - operation: atom (operator (sub_operations | operation))* + eval: "{" ((operation | rest) WS?)+ "}" + operation: prefix? atom (operator (sub_operations | operation))* sub_operations: "(" operation ")" - atom: (NUMBER | DECIMAL) + atom: number - %import common.NUMBER - //%import common.SIGNED_NUMBER - %import common.DECIMAL %import common.WS \ No newline at end of file