Source code for formulas.tokens.function

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
#
# Copyright 2016-2026 European Commission (JRC);
# Licensed under the EUPL (the 'Licence');
# You may not use this work except in compliance with the Licence.
# You may obtain a copy of the Licence at: http://ec.europa.eu/idabc/eupl

"""
It provides Function classes.
"""

import collections
import functools

# noinspection PyCompatibility
import regex
import schedula as sh

from formulas.functions import wrap_func
from . import Token
from .operand import Range
from .operator import Separator
from .parenthesis import Parenthesis
from ..errors import TokenError, FormulaError, FoundError


[docs] class Function(Token): _re = regex.compile( r'^\s*(?P<name>[A-Z_][\w\.]*)\(\s*(?P<call>\))?', regex.IGNORECASE ) def _process(self, attr, match, context=None, parser=None): if getattr(parser, 'is_cell', False): from formulas.functions import get_functions fn = attr['name'].upper() if fn not in get_functions(): attr['func_token'] = Range(fn, context, parser) return attr
[docs] def process(self, match, context=None, parser=None): attr = super(Function, self).process(match, context, parser) return self._process(attr, match, context, parser)
def _ast(self, tokens, stack, builder): has_call = self.has_call if self.has_func_token: fake = tokens[-2:] self.get_func_token.ast(fake, stack, builder) if not has_call: Separator(',').ast(fake, stack, builder) if has_call: Parenthesis(')').ast(tokens, stack, builder)
[docs] def ast(self, tokens, stack, builder, check_n=lambda *args: True): super(Function, self).ast(tokens, stack, builder) stack.append(self) t = Parenthesis('(') t.attr['check_n'] = check_n t.ast(tokens, stack, builder) self._ast(tokens, stack, builder)
[docs] def compile(self): from formulas.functions import get_functions if self.attr.get('func_token'): return run_function return get_functions()[self.name.upper()]
[docs] def set_expr(self, *tokens): args = ', '.join(t.get_expr for t in tokens[int(self.has_func_token):]) self.attr['expr'] = '%s(%s)' % (self.name.upper(), args)
_re_lambda = regex.compile(r''' ^\s*(?P<name>(?:_XLFN\.)?(?:LAMBDA|(?P<let>LET)))\(\s* (?: (?P<arg> # 1° argomento (?: [^()",]+ # testo normale (niente " ( ) ,) | "(?:[^"]|"")*" # stringa Excel: "" è apice doppio escapato | (?P<paren1> # parentesi annidate \( (?: [^()"]+ | "(?:[^"]|"")*" | (?P>paren1) )* \) ) )+ ) (?:\s*,\s* # , argomenti successivi (?P<arg> (?: [^()",]+ | "(?:[^"]|"")*" | (?P<paren2> \( (?: [^()"]+ | "(?:[^"]|"")*" | (?P>paren2) )* \) ) )+ ) )* )? \s*\)(?P<call>\()? | ^\s*(?P<name>_XLETA\.)(?P<leta>[[:alpha:]_\\]+[[:alnum:]\.\_\\]*) ''', regex.IGNORECASE | regex.X)
[docs] class LambdaFunction(functools.partial): def __new__(cls, __func, *args, **kwargs): return super(LambdaFunction, cls).__new__( cls, wrap_func(__func), *args, **kwargs ) def __repr__(self): from formulas.functions import Error return Error.errors['#VALUE!'] def __call__(self, *args, **kwargs): if callable(self.func): return super().__call__(*args, **kwargs) from formulas.functions import Error raise FoundError(err=Error.errors['#NAME?'])
[docs] class LetaFunction(LambdaFunction): pass
[docs] @wrap_func def run_function(fun, *args, **kwargs): if callable(fun): return fun(*args, **kwargs) from formulas.functions import Error raise FoundError(err=Error.errors['#NAME?'])
[docs] class Lambda(Function): _re = _re_lambda def _process(self, attr, match, context=None, parser=None): if 'leta' in attr: return attr tokens = [] vars = [] arg = match.captures('arg') separator = Separator(',') calc_tkns, calc = parser.ast(f"={arg[-1]}", context=context) expr = [] ext_inp = {} try: if 'let' in attr: out_id = calc.get_node_id(calc[-1]) valids = set(calc.dsp.shrink_dsp(outputs=[out_id]).data_nodes) for var, code in zip(arg[:-1:2], arg[1:-1:2]): var = Range(var, context, parser) vars.append(var) tkns, bld = parser.ast(f"={code}", context=context) tokens.extend((var, separator, *tkns, separator)) o_id = bld.get_node_id(bld[-1]) var_name = var.name expr.extend((var_name, ', ', o_id, ', ')) if var_name in valids: ext_inp.update( (t.name, t) for t in tkns if isinstance(t, Range) ) calc.dsp.add_function( function_id=f"={o_id}", function=sh.bypass, inputs=[o_id], outputs=[var_name], ) calc.dsp.extend(bld.dsp) else: for var in arg[:-1]: var = Range(var, context, parser) vars.append(var) tokens.extend((var, separator)) expr.extend((var.name, ', ')) except TokenError: raise FormulaError() tokens.extend((*calc_tkns, Parenthesis(')'))) expr.extend((calc.get_node_id(calc[-1]), ')')) ext_inp.update( (t.name, t) for t in calc_tkns if isinstance(t, Range) ) for t in vars: if not t.get_is_reference: raise FormulaError() ext_inp.pop(t.name, None) if 'call' in attr: tokens.append(Parenthesis('(')) expr.extend('(') attr['func'] = { 'expr': ''.join(expr), 'external_inputs': collections.OrderedDict(sorted( ext_inp.items() )), 'tokens': tokens, 'calculation': calc, 'vars': [t.name for t in vars] } return attr def _ast(self, tokens, stack, builder): fake = tokens[-2:] if self.has_leta: tokens.pop() Parenthesis(')').ast(fake, stack, builder) else: func = self.get_func for i, token in enumerate(func['external_inputs'].values()): if i: Separator(',').ast(fake, stack, builder) token.ast(fake, stack, builder) if self.has_call: Separator(',').ast(fake, stack, builder) else: Parenthesis(')').ast(fake, stack, builder) tokens.extend(func['tokens'])
[docs] def compile(self): if self.has_leta: from formulas.functions import get_functions func = get_functions()[self.get_leta.upper()] if isinstance(func, dict): func = func.copy() func['function'] = functools.partial( LetaFunction, func['function'] ) return func return functools.partial(LetaFunction, func) else: attr = self.get_func func = attr['calculation'].compile() if self.has_let: return func for x in attr['vars']: func.inputs.pop(x, None) func.inputs[x] = None if self.has_call: return wrap_func(func) return functools.partial(LambdaFunction, func)
[docs] def set_expr(self, *tokens): func = self.name.upper() if self.has_leta: func = f"{func}{self.get_leta.upper()}" else: d = self.get_func func = f"{func}({d['expr']}" if self.has_call: n = len(d['external_inputs']) func = f"{func}{', '.join(t.get_expr for t in tokens[n:])})" self.attr['expr'] = func
def _check_tkn_n_args(n_args, token): return token.n_args == n_args
[docs] class Array(Function): _re = regex.compile(r'^\s*(?P<name>(?P<start>{)|(?P<end>})|(?P<sep>;))\s*') def _process(self, attr, match, context=None, parser=None): return attr
[docs] def ast(self, tokens, stack, builder, check_n=lambda t: t.n_args): if self.has_start: Function('ARRAY(').ast(tokens, stack, builder, check_n=check_n) Function('ARRAY(').ast(tokens, stack, builder, check_n=check_n) else: token = Parenthesis(')') token.ast(tokens, stack, builder) if self.has_sep: check_n = functools.partial(_check_tkn_n_args, token.get_n_args) Function('ARRAY(').ast(tokens, stack, builder, check_n=check_n) else: Parenthesis(')').ast(tokens, stack, builder)