2018-01-21 08:26:34 +00:00
|
|
|
# vim:fenc=utf-8
|
|
|
|
#
|
|
|
|
# Copyright © 2017 lafrite <lafrite@Poivre>
|
|
|
|
#
|
|
|
|
# Distributed under terms of the MIT license.
|
|
|
|
|
|
|
|
"""
|
|
|
|
Tree class
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
from .tree_tools import (to_nested_parenthesis,
|
2018-03-13 11:43:48 +00:00
|
|
|
postfix_concatenate,
|
|
|
|
show_tree,
|
|
|
|
)
|
|
|
|
from .coroutine import coroutine, STOOOP
|
2018-01-21 08:26:34 +00:00
|
|
|
from .str2 import str2
|
|
|
|
from .operator import OPERATORS
|
|
|
|
|
|
|
|
__all__ = ["Tree", "MutableTree"]
|
|
|
|
|
|
|
|
|
|
|
|
class Tree(object):
|
|
|
|
|
|
|
|
"""
|
|
|
|
Binary tree
|
|
|
|
|
|
|
|
This is the chosen structure to manage calculus in PyMath.
|
|
|
|
|
|
|
|
The class is not mutable to preserve its integrity.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, node, left_value, right_value):
|
|
|
|
"""
|
|
|
|
Initiate a tree with tuple (node, (left value, right value))
|
|
|
|
|
|
|
|
:example:
|
|
|
|
|
|
|
|
>>> t = Tree("+", 1, 2)
|
|
|
|
>>> t.node
|
|
|
|
'+'
|
|
|
|
>>> t.left_value
|
|
|
|
1
|
|
|
|
>>> t.right_value
|
|
|
|
2
|
|
|
|
"""
|
2018-01-21 14:05:54 +00:00
|
|
|
if node is None or right_value is None:
|
|
|
|
raise TypeError("Tree can't have an empty node or an empty right leaf")
|
2018-01-21 08:26:34 +00:00
|
|
|
|
|
|
|
self.node = node
|
|
|
|
|
|
|
|
self.left_value = left_value
|
|
|
|
self.right_value = right_value
|
|
|
|
|
|
|
|
@classmethod
|
2018-03-13 11:43:48 +00:00
|
|
|
def from_str(cls, expression, convert_to_mo=True):
|
2018-01-21 08:26:34 +00:00
|
|
|
""" Initiate a tree from an string expression
|
|
|
|
|
|
|
|
:example:
|
|
|
|
|
2018-01-28 15:31:21 +00:00
|
|
|
>>> t = Tree.from_str("2+3*4")
|
2018-01-21 08:26:34 +00:00
|
|
|
>>> print(t)
|
|
|
|
+
|
|
|
|
> 2
|
|
|
|
> *
|
|
|
|
| > 3
|
|
|
|
| > 4
|
2018-01-28 15:31:21 +00:00
|
|
|
>>> t = Tree.from_str("(2+3)*4")
|
2018-01-21 08:26:34 +00:00
|
|
|
>>> print(t)
|
|
|
|
*
|
|
|
|
> +
|
|
|
|
| > 2
|
|
|
|
| > 3
|
|
|
|
> 4
|
2018-02-02 15:33:08 +00:00
|
|
|
>>> t = Tree.from_str("2+3*n")
|
|
|
|
>>> print(t)
|
|
|
|
+
|
|
|
|
> 2
|
|
|
|
> *
|
|
|
|
| > 3
|
|
|
|
| > n
|
|
|
|
|
2018-01-21 08:26:34 +00:00
|
|
|
"""
|
2018-03-13 11:43:48 +00:00
|
|
|
t = MutableTree.from_str(expression, convert_to_mo)
|
2018-01-30 02:34:18 +00:00
|
|
|
return cls.from_any_tree(t)
|
2018-01-21 08:26:34 +00:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def from_nested_parenthesis(cls, nested_parenthesis):
|
|
|
|
"""
|
|
|
|
Initiate recursively a tree with tuple (node, (left value, right value))
|
|
|
|
|
|
|
|
:example:
|
|
|
|
|
|
|
|
>>> nested_par = ("+", (1, 2))
|
|
|
|
>>> t = Tree.from_nested_parenthesis(nested_par)
|
|
|
|
>>> t.node
|
|
|
|
'+'
|
|
|
|
>>> t.left_value
|
|
|
|
1
|
|
|
|
>>> t.right_value
|
|
|
|
2
|
|
|
|
>>> nested_par = ("+", (
|
|
|
|
... ("*", (3, 4)),
|
|
|
|
... 2))
|
|
|
|
>>> t = Tree.from_nested_parenthesis(nested_par)
|
|
|
|
>>> t.node
|
|
|
|
'+'
|
|
|
|
>>> type(t.left_value)
|
2018-01-21 08:32:17 +00:00
|
|
|
<class 'mapytex.calculus.core.tree.Tree'>
|
2018-01-21 08:26:34 +00:00
|
|
|
>>> t.right_value
|
|
|
|
2
|
|
|
|
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
nested_len = len(nested_parenthesis)
|
|
|
|
num_len = len(nested_parenthesis[1])
|
|
|
|
except TypeError:
|
|
|
|
raise ValueError("Nested parenthesis are not composed of lists")
|
|
|
|
|
|
|
|
if nested_len != 2 and \
|
|
|
|
num_len != 2:
|
|
|
|
raise ValueError("Nested parenthesis don't have right shape")
|
|
|
|
|
|
|
|
node = nested_parenthesis[0]
|
|
|
|
|
|
|
|
try:
|
2018-01-30 02:34:18 +00:00
|
|
|
left_value = cls.from_nested_parenthesis(nested_parenthesis[1][0])
|
2018-01-21 08:26:34 +00:00
|
|
|
except ValueError:
|
|
|
|
left_value = nested_parenthesis[1][0]
|
|
|
|
try:
|
2018-01-30 02:34:18 +00:00
|
|
|
right_value = cls.from_nested_parenthesis(nested_parenthesis[1][1])
|
2018-01-21 08:26:34 +00:00
|
|
|
except ValueError:
|
|
|
|
right_value = nested_parenthesis[1][1]
|
|
|
|
|
|
|
|
return cls(node, left_value, right_value)
|
|
|
|
|
|
|
|
@classmethod
|
2018-01-30 02:34:18 +00:00
|
|
|
def from_list(cls, node, leafs):
|
|
|
|
""" Initiate a balanced tree with one node and a list of leafs
|
|
|
|
|
|
|
|
:param node: node for all node of the tree
|
|
|
|
:param leafs: list of leafs
|
|
|
|
|
|
|
|
:example:
|
|
|
|
>>> t = Tree.from_list("+", [1, 2])
|
|
|
|
>>> print(t)
|
|
|
|
+
|
|
|
|
> 1
|
|
|
|
> 2
|
|
|
|
>>> t = Tree.from_list("+", [1, 2, 3])
|
|
|
|
>>> print(t)
|
|
|
|
+
|
|
|
|
> 1
|
|
|
|
> +
|
|
|
|
| > 2
|
|
|
|
| > 3
|
|
|
|
>>> t = Tree.from_list("+", [1, 2, 3, 4])
|
|
|
|
>>> print(t)
|
|
|
|
+
|
|
|
|
> +
|
|
|
|
| > 1
|
|
|
|
| > 2
|
|
|
|
> +
|
|
|
|
| > 3
|
|
|
|
| > 4
|
|
|
|
>>> t = Tree.from_list("+", [1, 2])
|
|
|
|
>>> t2 = Tree.from_list("*", [1, t])
|
|
|
|
>>> print(t2)
|
|
|
|
*
|
|
|
|
> 1
|
|
|
|
> +
|
|
|
|
| > 1
|
|
|
|
| > 2
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
len_leafs = len(leafs)
|
|
|
|
if len_leafs < 2:
|
|
|
|
raise ValueError(f"Not enough leafs. Need at least 2 got {len(leafs)}")
|
|
|
|
elif len_leafs == 2:
|
|
|
|
l_value = leafs[0]
|
|
|
|
r_value = leafs[1]
|
|
|
|
elif len_leafs == 3:
|
|
|
|
l_value = leafs[0]
|
|
|
|
r_value = cls.from_list(node, leafs[1:])
|
|
|
|
else:
|
|
|
|
l_value = cls.from_list(node, leafs[:len_leafs//2])
|
|
|
|
r_value = cls.from_list(node, leafs[len_leafs//2:])
|
|
|
|
return cls(node, l_value, r_value)
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def from_any_tree(cls, tree):
|
|
|
|
""" Initial a Tree from an other type of tree (except LeafTree)
|
|
|
|
|
|
|
|
It also work to initiate MutableTree, AssocialTree or LeafTree from
|
|
|
|
any tree.
|
2018-01-21 08:26:34 +00:00
|
|
|
|
|
|
|
:example:
|
|
|
|
>>> t = MutableTree("*", 1, 2)
|
2018-01-30 02:34:18 +00:00
|
|
|
>>> print(Tree.from_any_tree(t))
|
2018-01-21 08:26:34 +00:00
|
|
|
*
|
|
|
|
> 1
|
|
|
|
> 2
|
|
|
|
>>> t1 = MutableTree("*", 1, 2)
|
|
|
|
>>> t2 = MutableTree("*", t1, 3)
|
2018-01-30 02:34:18 +00:00
|
|
|
>>> print(Tree.from_any_tree(t2))
|
2018-01-21 08:26:34 +00:00
|
|
|
*
|
|
|
|
> *
|
|
|
|
| > 1
|
|
|
|
| > 2
|
|
|
|
> 3
|
|
|
|
>>> t = MutableTree("*", 1)
|
|
|
|
>>> print(t)
|
|
|
|
*
|
|
|
|
> 1
|
|
|
|
> None
|
2018-01-30 02:34:18 +00:00
|
|
|
>>> Tree.from_any_tree(t)
|
2018-01-21 08:26:34 +00:00
|
|
|
Traceback (most recent call last):
|
|
|
|
...
|
|
|
|
TypeError: Tree can't have empty node or leaf
|
2018-01-30 02:34:18 +00:00
|
|
|
>>> tl = LeafTree("/", 1, 4)
|
|
|
|
>>> t2 = MutableTree("*", tl, 3)
|
|
|
|
>>> t = Tree.from_any_tree(t2)
|
|
|
|
>>> type(t)
|
|
|
|
<class 'mapytex.calculus.core.tree.Tree'>
|
|
|
|
>>> type(t.left_value)
|
|
|
|
<class 'mapytex.calculus.core.tree.LeafTree'>
|
2018-01-21 08:26:34 +00:00
|
|
|
|
|
|
|
"""
|
2018-01-30 02:34:18 +00:00
|
|
|
node = tree.node
|
|
|
|
left_value = tree.left_value
|
|
|
|
right_value = tree.right_value
|
2018-01-21 08:26:34 +00:00
|
|
|
|
|
|
|
if node is None or \
|
|
|
|
left_value is None or \
|
|
|
|
right_value is None:
|
|
|
|
raise TypeError("Tree can't have empty node or leaf")
|
|
|
|
|
|
|
|
try:
|
2018-01-30 02:34:18 +00:00
|
|
|
left_value.IMLEAF
|
2018-01-21 08:26:34 +00:00
|
|
|
except AttributeError:
|
2018-01-30 02:34:18 +00:00
|
|
|
try:
|
|
|
|
l_value = cls.from_any_tree(left_value)
|
|
|
|
except AttributeError:
|
|
|
|
l_value = left_value
|
|
|
|
else:
|
2018-01-21 08:26:34 +00:00
|
|
|
l_value = left_value
|
|
|
|
|
|
|
|
try:
|
2018-01-30 02:34:18 +00:00
|
|
|
right_value.IMLEAF
|
2018-01-21 08:26:34 +00:00
|
|
|
except AttributeError:
|
2018-01-30 02:34:18 +00:00
|
|
|
try:
|
|
|
|
r_value = cls.from_any_tree(right_value)
|
|
|
|
except AttributeError:
|
|
|
|
r_value = right_value
|
|
|
|
else:
|
2018-01-21 08:26:34 +00:00
|
|
|
r_value = right_value
|
|
|
|
|
|
|
|
return cls(node, l_value, r_value)
|
|
|
|
|
2018-01-21 09:14:16 +00:00
|
|
|
def map_on_leaf(self, function):
|
2018-01-21 08:26:34 +00:00
|
|
|
""" Map on leafs a function
|
|
|
|
|
|
|
|
:param function: take leaf value returns other value
|
|
|
|
:returns: Tree with calculated leaf
|
|
|
|
|
|
|
|
:example:
|
|
|
|
|
2018-03-13 11:43:48 +00:00
|
|
|
>>> t = Tree.from_str("3*4+2", convert_to_mo=False)
|
2018-01-21 08:52:22 +00:00
|
|
|
>>> print(t)
|
|
|
|
+
|
|
|
|
> *
|
|
|
|
| > 3
|
|
|
|
| > 4
|
|
|
|
> 2
|
2018-01-21 09:14:16 +00:00
|
|
|
>>> print(t.map_on_leaf(lambda x:2*x))
|
2018-01-21 08:52:22 +00:00
|
|
|
+
|
|
|
|
> *
|
|
|
|
| > 6
|
|
|
|
| > 8
|
|
|
|
> 4
|
2018-01-21 08:26:34 +00:00
|
|
|
|
|
|
|
"""
|
|
|
|
try:
|
2018-01-21 09:14:16 +00:00
|
|
|
left_applied = self.left_value.map_on_leaf(function)
|
2018-01-21 08:26:34 +00:00
|
|
|
except AttributeError:
|
|
|
|
left_applied = function(self.left_value)
|
|
|
|
|
|
|
|
try:
|
2018-01-21 09:14:16 +00:00
|
|
|
right_applied = self.right_value.map_on_leaf(function)
|
2018-01-21 08:26:34 +00:00
|
|
|
except AttributeError:
|
|
|
|
right_applied = function(self.right_value)
|
|
|
|
|
|
|
|
return Tree(self.node, left_applied, right_applied)
|
|
|
|
|
2018-01-21 09:14:16 +00:00
|
|
|
def apply_on_last_level(self, function):
|
|
|
|
""" Apply the function on last level of the tree before leaf
|
2018-01-21 08:26:34 +00:00
|
|
|
|
|
|
|
:param function: (op, a, a) -> b
|
|
|
|
:returns: b if it is a 1 level Tree, Tree otherwise
|
|
|
|
|
2018-01-21 09:10:45 +00:00
|
|
|
|
|
|
|
:example:
|
|
|
|
|
2018-01-30 02:34:18 +00:00
|
|
|
>>> t = Tree.from_str("3*4+2")
|
2018-01-21 09:10:45 +00:00
|
|
|
>>> print(t)
|
|
|
|
+
|
|
|
|
> *
|
|
|
|
| > 3
|
|
|
|
| > 4
|
|
|
|
> 2
|
|
|
|
>>> from .tree_tools import infix_str_concatenate
|
2018-01-21 09:14:16 +00:00
|
|
|
>>> tt = t.apply_on_last_level(lambda *x: infix_str_concatenate(*x))
|
2018-01-21 09:10:45 +00:00
|
|
|
>>> print(tt)
|
|
|
|
+
|
|
|
|
> 3 * 4
|
|
|
|
> 2
|
2018-03-11 15:34:41 +00:00
|
|
|
>>> tt = t.apply_on_last_level(lambda n, l, r: eval(str(l) + n + str(r)))
|
2018-01-21 15:16:08 +00:00
|
|
|
>>> print(tt)
|
|
|
|
+
|
|
|
|
> 12
|
|
|
|
> 2
|
2018-03-11 15:34:41 +00:00
|
|
|
>>> ttt = tt.apply_on_last_level(lambda n, l, r: eval(str(l) + n + str(r)))
|
2018-01-21 15:16:08 +00:00
|
|
|
>>> print(ttt)
|
|
|
|
14
|
2018-01-21 08:26:34 +00:00
|
|
|
"""
|
2018-01-21 09:10:45 +00:00
|
|
|
left_is_leaf = 0
|
|
|
|
right_is_leaf = 0
|
2018-01-21 08:26:34 +00:00
|
|
|
try:
|
2018-01-21 09:14:16 +00:00
|
|
|
left_applied = self.left_value.apply_on_last_level(function)
|
2018-01-21 08:26:34 +00:00
|
|
|
except AttributeError:
|
2018-01-21 09:10:45 +00:00
|
|
|
left_applied = self.left_value
|
2018-01-21 08:26:34 +00:00
|
|
|
left_is_leaf = 1
|
|
|
|
|
|
|
|
try:
|
2018-01-21 09:14:16 +00:00
|
|
|
right_applied = self.right_value.apply_on_last_level(function)
|
2018-01-21 08:26:34 +00:00
|
|
|
except AttributeError:
|
2018-01-21 09:10:45 +00:00
|
|
|
right_applied = self.right_value
|
2018-01-21 08:26:34 +00:00
|
|
|
right_is_leaf = 1
|
|
|
|
|
|
|
|
if left_is_leaf and right_is_leaf:
|
|
|
|
return function(self.node, self.left_value, self.right_value)
|
|
|
|
else:
|
|
|
|
return Tree(self.node, left_applied, right_applied)
|
|
|
|
|
|
|
|
def apply(self, function):
|
2018-01-21 09:14:16 +00:00
|
|
|
""" Apply the function on every node of the tree
|
2018-01-21 08:26:34 +00:00
|
|
|
|
|
|
|
:param function: (op, a, a) -> b
|
|
|
|
:returns: b
|
|
|
|
|
|
|
|
:example:
|
|
|
|
|
|
|
|
>>> def to_nested(op, left, right):
|
|
|
|
... return (op, (left, right))
|
|
|
|
>>> nested_par = ("+", (1, 2))
|
|
|
|
>>> t = Tree.from_nested_parenthesis(nested_par)
|
|
|
|
>>> t.apply(to_nested)
|
|
|
|
('+', (1, 2))
|
2018-01-30 02:34:18 +00:00
|
|
|
>>> assert t.apply(to_nested) == nested_par
|
2018-01-21 08:26:34 +00:00
|
|
|
|
|
|
|
>>> nested_par = ("+", (
|
|
|
|
... ("*", (3, 4)),
|
|
|
|
... 2))
|
|
|
|
>>> t = Tree.from_nested_parenthesis(nested_par)
|
|
|
|
>>> t.apply(to_nested)
|
|
|
|
('+', (('*', (3, 4)), 2))
|
2018-01-30 02:34:18 +00:00
|
|
|
>>> assert t.apply(to_nested) == nested_par
|
2018-01-21 08:26:34 +00:00
|
|
|
|
2018-03-12 17:01:52 +00:00
|
|
|
>>> t.apply(lambda n, l, r: eval(str(l) + n + str(r)))
|
2018-01-21 15:16:08 +00:00
|
|
|
14
|
|
|
|
|
2018-01-21 08:26:34 +00:00
|
|
|
"""
|
|
|
|
try:
|
|
|
|
left_value = self.left_value.apply(function)
|
|
|
|
except AttributeError:
|
|
|
|
left_value = self.left_value
|
|
|
|
|
|
|
|
try:
|
|
|
|
right_value = self.right_value.apply(function)
|
|
|
|
except AttributeError:
|
|
|
|
right_value = self.right_value
|
|
|
|
|
|
|
|
return function(self.node, left_value, right_value)
|
|
|
|
|
2018-03-13 11:43:48 +00:00
|
|
|
def get_leafs(self, callback=lambda x:x):
|
2018-01-28 15:13:37 +00:00
|
|
|
""" Generator which yield all the leaf value of the tree.
|
|
|
|
Callback act on every leaf.
|
|
|
|
|
|
|
|
:param callback: function on leaf
|
|
|
|
|
|
|
|
:example:
|
|
|
|
|
2018-01-30 02:34:18 +00:00
|
|
|
>>> t = Tree.from_str("3+4+5*2")
|
2018-01-28 15:13:37 +00:00
|
|
|
>>> [l for l in t.get_leafs()]
|
2018-03-09 16:31:46 +00:00
|
|
|
[<MOnumber 3>, <MOnumber 4>, <MOnumber 5>, <MOnumber 2>]
|
2018-01-28 15:13:37 +00:00
|
|
|
>>> {type(l) for l in t.get_leafs()}
|
2018-03-09 16:31:46 +00:00
|
|
|
{<class 'mapytex.calculus.core.MO.mo.MOnumber'>}
|
2018-01-28 15:13:37 +00:00
|
|
|
"""
|
|
|
|
try:
|
|
|
|
yield from self.left_value.get_leafs(callback)
|
|
|
|
except AttributeError:
|
|
|
|
yield callback(self.left_value)
|
|
|
|
try:
|
|
|
|
yield from self.right_value.get_leafs(callback)
|
|
|
|
except AttributeError:
|
|
|
|
yield callback(self.right_value)
|
|
|
|
|
2018-03-13 11:43:48 +00:00
|
|
|
def get_nodes(self, callback=lambda x:x):
|
2018-01-28 15:13:37 +00:00
|
|
|
""" Generator which yield all nodes of the tree.
|
|
|
|
Callback act on every nodes.
|
|
|
|
|
|
|
|
:param callback: function on node
|
|
|
|
|
|
|
|
:example:
|
|
|
|
|
2018-01-28 16:13:55 +00:00
|
|
|
>>> t = Tree.from_str('3*4+2')
|
2018-01-28 15:13:37 +00:00
|
|
|
>>> [l for l in t.get_nodes()]
|
|
|
|
['+', '*']
|
2018-01-28 16:13:55 +00:00
|
|
|
>>> t = Tree.from_str('3*4+3*4')
|
2018-01-28 15:13:37 +00:00
|
|
|
>>> [l for l in t.get_nodes()]
|
|
|
|
['+', '*', '*']
|
|
|
|
"""
|
|
|
|
yield self.node
|
|
|
|
try:
|
|
|
|
yield from self.left_value.get_nodes(callback)
|
|
|
|
except AttributeError:
|
|
|
|
pass
|
|
|
|
try:
|
|
|
|
yield from self.right_value.get_nodes(callback)
|
|
|
|
except AttributeError:
|
|
|
|
pass
|
|
|
|
|
2018-01-28 16:13:55 +00:00
|
|
|
def depth(self):
|
|
|
|
""" Return the depth of the tree
|
|
|
|
|
|
|
|
:example:
|
|
|
|
|
|
|
|
>>> nested_par = ("+", (1, 2))
|
|
|
|
>>> t = Tree.from_nested_parenthesis(nested_par)
|
|
|
|
>>> t.depth()
|
|
|
|
1
|
|
|
|
>>> nested_par = ("+", (
|
|
|
|
... ("*", (3, 4)),
|
|
|
|
... 2))
|
|
|
|
>>> t = Tree.from_nested_parenthesis(nested_par)
|
|
|
|
>>> t.depth()
|
|
|
|
2
|
|
|
|
>>> nested_par = ("+", (
|
|
|
|
... ("*", (3,
|
|
|
|
... ("/", (4, 4))
|
|
|
|
... )),
|
|
|
|
... 2))
|
|
|
|
>>> t = Tree.from_nested_parenthesis(nested_par)
|
|
|
|
>>> t.depth()
|
|
|
|
3
|
|
|
|
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
l_depth = self.left_value.depth()
|
|
|
|
except AttributeError:
|
|
|
|
l_depth = 0
|
|
|
|
|
|
|
|
try:
|
|
|
|
r_depth = self.right_value.depth()
|
|
|
|
except AttributeError:
|
|
|
|
r_depth = 0
|
|
|
|
|
|
|
|
return 1 + max(l_depth, r_depth)
|
|
|
|
|
2018-01-21 08:26:34 +00:00
|
|
|
def __str__(self):
|
|
|
|
""" Overload str method
|
|
|
|
|
|
|
|
:example:
|
|
|
|
|
|
|
|
>>> nested_par = ("+", (1, 2))
|
|
|
|
>>> t = Tree.from_nested_parenthesis(nested_par)
|
|
|
|
>>> print(t)
|
|
|
|
+
|
|
|
|
> 1
|
|
|
|
> 2
|
|
|
|
>>> nested_par = ("+", (
|
|
|
|
... ("*", (3, 4)),
|
|
|
|
... 2))
|
|
|
|
>>> t = Tree.from_nested_parenthesis(nested_par)
|
|
|
|
>>> print(t)
|
|
|
|
+
|
|
|
|
> *
|
|
|
|
| > 3
|
|
|
|
| > 4
|
|
|
|
> 2
|
|
|
|
>>> nested_par = ("+", (
|
|
|
|
... ("*", (1, 2)),
|
|
|
|
... ("*", (3, 4)),
|
|
|
|
... ))
|
|
|
|
>>> t = Tree.from_nested_parenthesis(nested_par)
|
|
|
|
>>> print(t)
|
|
|
|
+
|
|
|
|
> *
|
|
|
|
| > 1
|
|
|
|
| > 2
|
|
|
|
> *
|
|
|
|
| > 3
|
|
|
|
| > 4
|
|
|
|
"""
|
|
|
|
return self.apply(show_tree)
|
|
|
|
|
2018-01-29 04:51:54 +00:00
|
|
|
def to_nested_parenthesis(self):
|
|
|
|
""" Transform the Tree into its nested parenthesis description
|
|
|
|
:example:
|
|
|
|
|
|
|
|
>>> nested_par = ("+", (1, 2))
|
|
|
|
>>> t = Tree.from_nested_parenthesis(nested_par)
|
|
|
|
>>> t.to_nested_parenthesis()
|
|
|
|
('+', (1, 2))
|
|
|
|
>>> nested_par = ("+", (
|
|
|
|
... ("*", (3, 4)),
|
|
|
|
... 2))
|
|
|
|
>>> t = Tree.from_nested_parenthesis(nested_par)
|
|
|
|
>>> t.to_nested_parenthesis()
|
|
|
|
('+', (('*', (3, 4)), 2))
|
|
|
|
"""
|
|
|
|
return self.apply(to_nested_parenthesis)
|
|
|
|
|
|
|
|
def to_postfix(self):
|
|
|
|
""" Transform the Tree into postfix notation
|
|
|
|
|
|
|
|
:example:
|
|
|
|
|
|
|
|
>>> nested_par = ("+", (1, 2))
|
|
|
|
>>> t = Tree.from_nested_parenthesis(nested_par)
|
|
|
|
>>> t.to_postfix()
|
|
|
|
[1, 2, '+']
|
|
|
|
>>> nested_par = ("+", (
|
|
|
|
... ("*", (3, 4)),
|
|
|
|
... 2))
|
|
|
|
>>> t = Tree.from_nested_parenthesis(nested_par)
|
|
|
|
>>> t.to_postfix()
|
|
|
|
[3, 4, '*', 2, '+']
|
|
|
|
>>> nested_par = ("+", (
|
|
|
|
... ("*", (3, 4)),
|
|
|
|
... ("*", (5, 6)),
|
|
|
|
... ))
|
|
|
|
>>> t = Tree.from_nested_parenthesis(nested_par)
|
|
|
|
>>> t.to_postfix()
|
|
|
|
[3, 4, '*', 5, 6, '*', '+']
|
|
|
|
"""
|
|
|
|
return self.apply(postfix_concatenate)
|
|
|
|
|
2018-01-21 08:26:34 +00:00
|
|
|
class MutableTree(Tree):
|
|
|
|
|
|
|
|
"""
|
|
|
|
Mutable Tree.
|
|
|
|
|
|
|
|
It is used to build a new tree before fixing it into a Tree
|
|
|
|
|
|
|
|
"""
|
|
|
|
def __init__(self,
|
|
|
|
node = None,
|
|
|
|
left_value = None,
|
|
|
|
right_value = None):
|
|
|
|
"""
|
|
|
|
Initiate a tree with potentialy None values
|
|
|
|
|
|
|
|
:Example:
|
|
|
|
|
|
|
|
>>> t = MutableTree()
|
|
|
|
>>> t.node, t.left_value, t.right_value
|
|
|
|
(None, None, None)
|
|
|
|
>>> t = MutableTree('*', 1)
|
|
|
|
>>> t.node, t.left_value, t.right_value
|
|
|
|
('*', 1, None)
|
|
|
|
"""
|
|
|
|
|
|
|
|
self.node = node
|
|
|
|
|
|
|
|
self.left_value = left_value
|
|
|
|
self.right_value = right_value
|
|
|
|
|
|
|
|
@classmethod
|
2018-03-13 11:43:48 +00:00
|
|
|
def from_str(cls, expression, convert_to_mo=True):
|
2018-01-21 08:26:34 +00:00
|
|
|
""" Initiate the MutableTree
|
|
|
|
|
|
|
|
:example:
|
2018-01-28 15:31:21 +00:00
|
|
|
>>> t = MutableTree.from_str("2+3*4")
|
2018-01-21 08:26:34 +00:00
|
|
|
>>> print(t)
|
|
|
|
+
|
|
|
|
> 2
|
|
|
|
> *
|
|
|
|
| > 3
|
|
|
|
| > 4
|
2018-01-28 15:31:21 +00:00
|
|
|
>>> t = MutableTree.from_str("(2+3)*4")
|
2018-01-21 08:26:34 +00:00
|
|
|
>>> print(t)
|
|
|
|
*
|
|
|
|
> +
|
|
|
|
| > 2
|
|
|
|
| > 3
|
|
|
|
> 4
|
2018-01-28 15:31:21 +00:00
|
|
|
>>> t = MutableTree.from_str("4*(-2+3)")
|
2018-01-21 08:26:34 +00:00
|
|
|
>>> print(t)
|
|
|
|
*
|
|
|
|
> 4
|
|
|
|
> +
|
2018-03-07 13:26:06 +00:00
|
|
|
| > -2
|
2018-01-21 08:26:34 +00:00
|
|
|
| > 3
|
|
|
|
"""
|
2018-03-13 11:43:48 +00:00
|
|
|
str_2_mut_tree = str2(cls.sink, convert_to_mo)
|
|
|
|
return str_2_mut_tree(expression)
|
2018-01-21 08:26:34 +00:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
@coroutine
|
|
|
|
def sink(cls):
|
|
|
|
""" Sink which build a build a mutabletree
|
|
|
|
|
|
|
|
Returns the built tree when STOOOP is thrown
|
|
|
|
|
|
|
|
:example:
|
|
|
|
>>> sink = MutableTree.sink()
|
|
|
|
>>> for i in ["1", "+", "2", "*", "3"]:
|
|
|
|
... sink.send(i)
|
|
|
|
>>> a = sink.throw(STOOOP)
|
|
|
|
>>> print(a)
|
|
|
|
+
|
|
|
|
> 1
|
|
|
|
> *
|
|
|
|
| > 2
|
|
|
|
| > 3
|
|
|
|
>>> sink = MutableTree.sink()
|
|
|
|
>>> for i in ["1", "*", "2", "+", "3"]:
|
|
|
|
... sink.send(i)
|
|
|
|
>>> a = sink.throw(STOOOP)
|
|
|
|
>>> print(a)
|
|
|
|
+
|
|
|
|
> *
|
|
|
|
| > 1
|
|
|
|
| > 2
|
|
|
|
> 3
|
|
|
|
>>> sink = MutableTree.sink()
|
|
|
|
>>> for i in ["-", "1", "+", "2"]:
|
|
|
|
... sink.send(i)
|
|
|
|
>>> a = sink.throw(STOOOP)
|
|
|
|
>>> print(a)
|
|
|
|
+
|
|
|
|
> -
|
|
|
|
| > None
|
|
|
|
| > 1
|
|
|
|
> 2
|
|
|
|
|
|
|
|
"""
|
|
|
|
ans = cls()
|
|
|
|
try:
|
|
|
|
while True:
|
|
|
|
c = yield
|
|
|
|
if c is not None:
|
|
|
|
if c in OPERATORS.keys():
|
|
|
|
try:
|
|
|
|
ans.set_node(c)
|
|
|
|
except ValueError:
|
2018-01-21 14:05:54 +00:00
|
|
|
if OPERATORS[c]["precedence"] > OPERATORS[ans.node]["precedence"]:
|
2018-01-21 08:26:34 +00:00
|
|
|
ans.append_bot(c)
|
|
|
|
else:
|
|
|
|
ans.append_top(c)
|
|
|
|
else:
|
|
|
|
ans.append(c)
|
|
|
|
except STOOOP:
|
|
|
|
yield ans
|
|
|
|
|
|
|
|
def set_node(self, value):
|
|
|
|
""" Set the node value.
|
|
|
|
Once it has been changed it can't be changed again.
|
|
|
|
|
|
|
|
:example:
|
|
|
|
>>> t = MutableTree()
|
|
|
|
>>> t.node
|
|
|
|
>>> t.set_node("*")
|
|
|
|
>>> t.node
|
|
|
|
'*'
|
|
|
|
>>> t.set_node("+")
|
|
|
|
Traceback (most recent call last):
|
|
|
|
...
|
|
|
|
ValueError: The node of the tree is already set
|
|
|
|
"""
|
|
|
|
if self.node is None:
|
|
|
|
self.node = value
|
|
|
|
else:
|
|
|
|
raise ValueError("The node of the tree is already set")
|
|
|
|
|
|
|
|
def set_left_value(self, value):
|
|
|
|
""" Set the left value.
|
|
|
|
Once it has been changed it can't be changed again.
|
|
|
|
|
|
|
|
:example:
|
|
|
|
>>> t = MutableTree()
|
|
|
|
>>> t.left_value
|
|
|
|
>>> t.set_left_value(1)
|
|
|
|
>>> t.left_value
|
|
|
|
1
|
|
|
|
>>> t.set_left_value(2)
|
|
|
|
Traceback (most recent call last):
|
|
|
|
...
|
|
|
|
ValueError: The left branch is full, use set_right_value
|
|
|
|
"""
|
|
|
|
if self.left_value is None:
|
|
|
|
self.left_value = value
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
self.left_value.append(value)
|
|
|
|
except AttributeError:
|
|
|
|
raise ValueError("The left branch is full, use set_right_value")
|
|
|
|
|
|
|
|
def set_right_value(self, value):
|
|
|
|
""" Set the right value.
|
|
|
|
Once it has been changed it can't be changed again.
|
|
|
|
|
|
|
|
:example:
|
|
|
|
>>> t = MutableTree()
|
|
|
|
>>> t.right_value
|
|
|
|
>>> t.set_right_value(2)
|
|
|
|
>>> t.right_value
|
|
|
|
2
|
|
|
|
>>> t.set_right_value(3)
|
|
|
|
Traceback (most recent call last):
|
|
|
|
...
|
|
|
|
ValueError: The right branch is full, use append_top or append_bot
|
|
|
|
>>> # potentielle source de problèmes??
|
|
|
|
>>> t = MutableTree()
|
|
|
|
>>> t.set_right_value([1])
|
|
|
|
>>> t.right_value
|
|
|
|
[1]
|
|
|
|
>>> t.set_right_value(3)
|
|
|
|
"""
|
|
|
|
if self.right_value is None:
|
|
|
|
self.right_value = value
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
self.right_value.append(value)
|
|
|
|
except AttributeError:
|
|
|
|
raise ValueError("The right branch is full, use append_top or append_bot")
|
|
|
|
|
|
|
|
def append(self, value):
|
|
|
|
""" Append the value at the bottom of the tree.
|
|
|
|
|
|
|
|
In order to enable operator with arity 1, left value can be set only
|
|
|
|
before the node is set, assuming that those operator are placed before
|
|
|
|
operand.
|
|
|
|
|
|
|
|
It tries to add it on left branch first.
|
|
|
|
If it fails, the value is appened on right branch.
|
|
|
|
|
|
|
|
:example:
|
|
|
|
>>> t = MutableTree()
|
|
|
|
>>> t.append(1)
|
|
|
|
>>> t.set_node("*")
|
|
|
|
>>> t.append(2)
|
|
|
|
>>> print(t)
|
|
|
|
*
|
|
|
|
> 1
|
|
|
|
> 2
|
|
|
|
>>> t.append(3)
|
|
|
|
Traceback (most recent call last):
|
|
|
|
...
|
|
|
|
ValueError: The right branch is full, use append_top or append_bot
|
|
|
|
|
|
|
|
>>> t1 = MutableTree()
|
|
|
|
>>> t1.append(1)
|
|
|
|
>>> t1.set_node("*")
|
|
|
|
>>> t2 = MutableTree()
|
|
|
|
>>> t1.append(t2)
|
|
|
|
>>> t1.append(2)
|
|
|
|
>>> t2.set_node("+")
|
|
|
|
>>> t1.append(3)
|
|
|
|
>>> print(t1)
|
|
|
|
*
|
|
|
|
> 1
|
|
|
|
> +
|
|
|
|
| > 2
|
|
|
|
| > 3
|
|
|
|
|
|
|
|
>>> t1 = MutableTree()
|
|
|
|
>>> t1.set_node("-")
|
|
|
|
>>> t1.append(1)
|
|
|
|
>>> print(t1)
|
|
|
|
-
|
|
|
|
> None
|
|
|
|
> 1
|
|
|
|
"""
|
|
|
|
if self.node is None:
|
|
|
|
try:
|
|
|
|
self.set_left_value(value)
|
|
|
|
except ValueError:
|
|
|
|
self.set_right_value(value)
|
|
|
|
else:
|
|
|
|
self.set_right_value(value)
|
|
|
|
|
|
|
|
def append_top(self, value):
|
|
|
|
""" Append node into the tree at the top
|
|
|
|
|
|
|
|
:example:
|
|
|
|
|
|
|
|
>>> t = MutableTree("*", 1, 2)
|
|
|
|
>>> t.append_top("+")
|
|
|
|
>>> print(t)
|
|
|
|
+
|
|
|
|
> *
|
|
|
|
| > 1
|
|
|
|
| > 2
|
|
|
|
> None
|
|
|
|
"""
|
|
|
|
|
|
|
|
self_cp = MutableTree()
|
|
|
|
self_cp.set_node(self.node)
|
|
|
|
self_cp.set_left_value(self.left_value)
|
|
|
|
self_cp.set_right_value(self.right_value)
|
|
|
|
|
|
|
|
self.left_value, self.node, self.right_value = self_cp, value, None
|
|
|
|
|
|
|
|
def append_bot(self, value):
|
|
|
|
""" Append node into the tree at the bottom right_value
|
|
|
|
|
|
|
|
:example:
|
|
|
|
|
|
|
|
>>> t = MutableTree("*", 1, 2)
|
|
|
|
>>> t.append_bot("+")
|
|
|
|
>>> print(t)
|
|
|
|
*
|
|
|
|
> 1
|
|
|
|
> +
|
|
|
|
| > 2
|
|
|
|
| > None
|
|
|
|
"""
|
|
|
|
|
|
|
|
rv = self.right_value
|
|
|
|
nright_value = MutableTree()
|
|
|
|
nright_value.set_node(value)
|
|
|
|
nright_value.set_left_value(rv)
|
|
|
|
|
|
|
|
self.right_value = nright_value
|
|
|
|
|
2018-01-29 04:51:54 +00:00
|
|
|
class LeafTree(Tree):
|
|
|
|
|
|
|
|
""" A LeafTree is a tree which act as if it is a leaf.
|
|
|
|
It blocks thoses methods:
|
|
|
|
- apply
|
|
|
|
- apply_on_last_level
|
|
|
|
"""
|
|
|
|
|
2018-01-30 02:34:18 +00:00
|
|
|
IMLEAF = 1
|
|
|
|
|
2018-01-29 04:51:54 +00:00
|
|
|
def apply(self, *args):
|
|
|
|
raise AttributeError("Can't use apply on a LeafTree")
|
|
|
|
|
|
|
|
def apply_on_last_level(self, *args):
|
2018-01-30 02:34:18 +00:00
|
|
|
raise AttributeError("Can't use apply_on_last_level on a LeafTree")
|
|
|
|
|
|
|
|
class AssocialTree(Tree):
|
|
|
|
|
|
|
|
""" Tree which concider every subtree with a node different from itself
|
2018-02-02 15:37:24 +00:00
|
|
|
as a Leaf
|
2018-01-30 02:34:18 +00:00
|
|
|
"""
|
|
|
|
def map_on_leaf(self, function):
|
|
|
|
""" Map on leafs a function
|
|
|
|
|
|
|
|
:param function: function on a single value or a tree
|
|
|
|
:returns: Tree with calculated leaf
|
|
|
|
|
|
|
|
:example:
|
|
|
|
|
2018-03-13 11:43:48 +00:00
|
|
|
>>> t = Tree.from_str("3*4+2", convert_to_mo=False)
|
2018-01-30 02:34:18 +00:00
|
|
|
>>> print(t)
|
|
|
|
+
|
|
|
|
> *
|
|
|
|
| > 3
|
|
|
|
| > 4
|
|
|
|
> 2
|
|
|
|
>>> print(t.map_on_leaf(lambda x:2*x))
|
|
|
|
+
|
|
|
|
> *
|
|
|
|
| > 6
|
|
|
|
| > 8
|
|
|
|
> 4
|
|
|
|
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
left_applied = self.left_value.map_on_leaf(function)
|
|
|
|
except AttributeError:
|
|
|
|
left_applied = function(self.left_value)
|
|
|
|
|
|
|
|
try:
|
|
|
|
right_applied = self.right_value.map_on_leaf(function)
|
|
|
|
except AttributeError:
|
|
|
|
right_applied = function(self.right_value)
|
|
|
|
|
|
|
|
return Tree(self.node, left_applied, right_applied)
|
|
|
|
|
|
|
|
def apply_on_last_level(self, function):
|
|
|
|
raise AttributeError("apply_on_last_level not available for AssocialTree")
|
|
|
|
|
|
|
|
def apply(self, function):
|
|
|
|
""" Apply the function on every node of the tree
|
|
|
|
|
|
|
|
:param function: (op, a, a) -> b
|
|
|
|
:returns: b
|
|
|
|
|
|
|
|
:example:
|
|
|
|
|
|
|
|
>>> def to_nested(op, left, right):
|
|
|
|
... try:
|
|
|
|
... l = f"tree({left.node})"
|
|
|
|
... except AttributeError:
|
|
|
|
... l = left
|
|
|
|
... try:
|
|
|
|
... r = f"tree({right.node})"
|
|
|
|
... except AttributeError:
|
|
|
|
... r = right
|
|
|
|
... return (op, (l, r))
|
|
|
|
>>> t = AssocialTree.from_str("3+4+5*2")
|
|
|
|
>>> t.apply(to_nested)
|
2018-03-09 16:31:46 +00:00
|
|
|
('+', (('+', (<MOnumber 3>, <MOnumber 4>)), 'tree(*)'))
|
2018-01-30 02:34:18 +00:00
|
|
|
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
if self.left_value.node == self.node:
|
|
|
|
left_value = self.left_value.apply(function)
|
|
|
|
else:
|
|
|
|
left_value = self.left_value
|
|
|
|
except AttributeError:
|
|
|
|
left_value = self.left_value
|
|
|
|
|
|
|
|
try:
|
|
|
|
if self.right_value.node == self.node:
|
|
|
|
right_value = self.right_value.apply(function)
|
|
|
|
else:
|
|
|
|
right_value = self.right_value
|
|
|
|
except AttributeError:
|
|
|
|
right_value = self.right_value
|
|
|
|
|
|
|
|
return function(self.node, left_value, right_value)
|
|
|
|
|
2018-03-13 11:43:48 +00:00
|
|
|
def get_leafs(self, callback=lambda x: x):
|
2018-01-30 02:34:18 +00:00
|
|
|
""" Generator which yield all the leaf value of the tree.
|
|
|
|
Callback act on every leaf.
|
|
|
|
|
|
|
|
:param callback: function on leaf or Tree
|
|
|
|
|
|
|
|
:example:
|
|
|
|
|
|
|
|
>>> t = AssocialTree.from_str("3+4+5*2")
|
|
|
|
>>> [l for l in t.get_leafs(str)]
|
|
|
|
['3', '4', '*\\n > 5\\n > 2']
|
|
|
|
>>> [ l for l in t.get_leafs(type) ]
|
2018-03-09 16:31:46 +00:00
|
|
|
[<class 'mapytex.calculus.core.MO.mo.MOnumber'>, <class 'mapytex.calculus.core.MO.mo.MOnumber'>, <class 'mapytex.calculus.core.tree.AssocialTree'>]
|
2018-01-30 02:34:18 +00:00
|
|
|
"""
|
|
|
|
try:
|
|
|
|
if self.left_value.node == self.node:
|
|
|
|
yield from self.left_value.get_leafs(callback)
|
|
|
|
else:
|
|
|
|
yield callback(self.left_value)
|
|
|
|
except AttributeError:
|
|
|
|
yield callback(self.left_value)
|
|
|
|
|
|
|
|
try:
|
|
|
|
if self.right_value.node == self.node:
|
|
|
|
yield from self.right_value.get_leafs(callback)
|
|
|
|
else:
|
|
|
|
yield callback(self.right_value)
|
|
|
|
except AttributeError:
|
|
|
|
yield callback(self.right_value)
|
2018-01-29 04:51:54 +00:00
|
|
|
|
2018-01-31 14:18:52 +00:00
|
|
|
def balance(self):
|
|
|
|
""" Balance the tree
|
|
|
|
|
|
|
|
:example:
|
|
|
|
>>> t = AssocialTree.from_str("1+2+3+4+5+6+7+8+9")
|
|
|
|
>>> print(t)
|
|
|
|
+
|
|
|
|
> +
|
|
|
|
| > +
|
|
|
|
| | > +
|
|
|
|
| | | > +
|
|
|
|
| | | | > +
|
|
|
|
| | | | | > +
|
|
|
|
| | | | | | > +
|
|
|
|
| | | | | | | > 1
|
|
|
|
| | | | | | | > 2
|
|
|
|
| | | | | | > 3
|
|
|
|
| | | | | > 4
|
|
|
|
| | | | > 5
|
|
|
|
| | | > 6
|
|
|
|
| | > 7
|
|
|
|
| > 8
|
|
|
|
> 9
|
|
|
|
>>> bal_t = t.balance()
|
|
|
|
>>> print(bal_t)
|
|
|
|
+
|
|
|
|
> +
|
|
|
|
| > +
|
|
|
|
| | > 1
|
|
|
|
| | > 2
|
|
|
|
| > +
|
|
|
|
| | > 3
|
|
|
|
| | > 4
|
|
|
|
> +
|
|
|
|
| > +
|
|
|
|
| | > 5
|
|
|
|
| | > 6
|
|
|
|
| > +
|
|
|
|
| | > 7
|
|
|
|
| | > +
|
|
|
|
| | | > 8
|
|
|
|
| | | > 9
|
|
|
|
>>> t = AssocialTree.from_str("1*2*3*4*5+6+7+8+9")
|
|
|
|
>>> print(t)
|
|
|
|
+
|
|
|
|
> +
|
|
|
|
| > +
|
|
|
|
| | > +
|
|
|
|
| | | > *
|
|
|
|
| | | | > *
|
|
|
|
| | | | | > *
|
|
|
|
| | | | | | > *
|
|
|
|
| | | | | | | > 1
|
|
|
|
| | | | | | | > 2
|
|
|
|
| | | | | | > 3
|
|
|
|
| | | | | > 4
|
|
|
|
| | | | > 5
|
|
|
|
| | | > 6
|
|
|
|
| | > 7
|
|
|
|
| > 8
|
|
|
|
> 9
|
|
|
|
>>> bal_t = t.balance()
|
|
|
|
>>> print(bal_t)
|
|
|
|
+
|
|
|
|
> +
|
|
|
|
| > *
|
|
|
|
| | > *
|
|
|
|
| | | > 1
|
|
|
|
| | | > 2
|
|
|
|
| | > *
|
|
|
|
| | | > 3
|
|
|
|
| | | > *
|
|
|
|
| | | | > 4
|
|
|
|
| | | | > 5
|
|
|
|
| > 6
|
|
|
|
> +
|
|
|
|
| > 7
|
|
|
|
| > +
|
|
|
|
| | > 8
|
|
|
|
| | > 9
|
|
|
|
|
|
|
|
|
|
|
|
:returns: Balanced Tree (not AssocialTree)
|
|
|
|
|
|
|
|
"""
|
|
|
|
leafs = self.get_leafs()
|
|
|
|
balanced_leafs = []
|
|
|
|
for l in leafs:
|
|
|
|
try:
|
|
|
|
balanced_leafs.append(l.balance())
|
|
|
|
except AttributeError:
|
|
|
|
balanced_leafs.append(l)
|
|
|
|
|
|
|
|
t = Tree.from_list(self.node, balanced_leafs)
|
|
|
|
return t
|
|
|
|
|
2018-01-21 08:26:34 +00:00
|
|
|
# -----------------------------
|
|
|
|
# Reglages pour 'vim'
|
|
|
|
# vim:set autoindent expandtab tabstop=4 shiftwidth=4:
|
|
|
|
# cursor: 16 del
|