ScoDoc/app/scodoc/sco_formulas.py

197 lines
5.6 KiB
Python
Raw Normal View History

2020-09-26 16:19:37 +02:00
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
2023-01-02 13:16:27 +01:00
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
2020-09-26 16:19:37 +02:00
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Une classe "vecteur" pour les formules utilisateurs de calcul des moyennes
"""
import operator
2021-07-09 17:47:06 +02:00
from functools import reduce
2020-09-26 16:19:37 +02:00
2021-07-09 23:19:30 +02:00
def cmp(x, y):
"""
Replacement for built-in function cmp that was removed in Python 3
Compare the two objects x and y and return an integer according to
the outcome. The return value is negative if x < y, zero if x == y
and strictly positive if x > y.
"""
return (x > y) - (x < y)
class NoteVector(object):
2020-09-26 16:19:37 +02:00
"""Vecteur de notes (ou coefficients) utilisé pour les formules définies par l'utilisateur.
Les éléments sont accessibles soit par index v[i], soit par leur nom v['nom'] s'il en ont un.
Les éléments sont toujours numériques (float). Les valeurs non numériques ('NI', ...) sont
considérées comme nulles (0.0).
"""
def __init__(self, *args, **kwargs):
if args:
2021-07-09 17:47:06 +02:00
self.v = list(map(float, args)) # cast to list of float
2020-09-26 16:19:37 +02:00
elif "v" in kwargs:
v = kwargs["v"]
if not isinstance(v, NoteVector):
# replace all non-numeric values by zeroes: (formulas should check cmask !)
for i in range(len(v)):
try:
v[i] = float(v[i])
2021-07-09 17:47:06 +02:00
except ValueError:
2020-09-26 16:19:37 +02:00
v[i] = 0.0
self.v = v
else:
self.v = []
self.name_idx = {} # { name : index in vector }
def __len__(self):
return len(self.v)
def __getitem__(self, i):
try:
return self.v[i]
except:
2021-07-11 18:18:44 +02:00
if isinstance(i, str):
2020-09-26 16:19:37 +02:00
return self.v[self.name_idx[i]]
else:
raise IndexError("index %s out of range" % i)
def append(self, value, name=None):
"""Append a value to the vector."""
try:
v = float(value)
2021-07-09 20:05:04 +02:00
except ValueError:
2020-09-26 16:19:37 +02:00
v = 0.0
self.v.append(v)
if name:
self.name_idx[name] = len(self.v) - 1
def __repr__(self):
return "NVector(%s, name_idx=%s)" % (str(self.v), self.name_idx)
def __add__(self, x):
return binary_op(self.v, x, operator.add)
__radd__ = __add__
def __sub__(self, x):
return binary_op(self.v, x, operator.sub)
def __rsub__(self, x):
return binary_op(x, self.v, operator.sub)
def __mul__(self, x):
return binary_op(self.v, x, operator.mul)
__rmul__ = __mul__
def __div__(self, x):
2021-07-09 20:05:04 +02:00
return binary_op(self.v, x, operator.truediv)
__truediv__ = __div__ # for py3
def __floordiv__(self, x):
return binary_op(self.v, x, operator.floordiv)
2020-09-26 16:19:37 +02:00
def __rdiv__(self, x):
2021-07-09 20:05:04 +02:00
return binary_op(x, self.v, operator.truediv)
__rtruediv__ = __rdiv__ # for py3
def __rfloordiv__(self, x):
return binary_op(x, self.v, operator.floordiv)
2020-09-26 16:19:37 +02:00
2021-07-11 22:32:01 +02:00
def is_scalar(x):
return isinstance(x, (float, int))
2020-09-26 16:19:37 +02:00
def binary_op(x, y, op):
2021-07-11 22:32:01 +02:00
if is_scalar(x):
if is_scalar(y):
2020-09-26 16:19:37 +02:00
x, y = [x], [y]
else:
x = [x] * len(y)
2021-07-11 22:32:01 +02:00
if is_scalar(y):
2020-09-26 16:19:37 +02:00
y = [y] * len(x)
if len(x) != len(y):
raise ValueError("vectors sizes don't match")
return NoteVector(v=[op(a, b) for (a, b) in zip(x, y)])
def dot(u, v):
"""Dot product between 2 lists or vectors"""
return sum([x * y for (x, y) in zip(u, v)])
def ternary_op(cond, a, b):
if cond:
return a
else:
return b
def geometrical_mean(v, w=None):
"""Geometrical mean of v, with optional weights w"""
if w is None:
return pow(reduce(operator.mul, v), 1.0 / len(v))
else:
if len(w) != len(v):
raise ValueError("vectors sizes don't match")
vw = [pow(x, y) for (x, y) in zip(v, w)]
return pow(reduce(operator.mul, vw), 1.0 / sum(w))
# Les builtins autorisées dans les formules utilisateur:
formula_builtins = {
"V": NoteVector,
"dot": dot,
"max": max,
"min": min,
"abs": abs,
"cmp": cmp,
"len": len,
"map": map,
"pow": pow,
"reduce": reduce,
"round": round,
"sum": sum,
"ifelse": ternary_op,
"geomean": geometrical_mean,
}
# v = NoteVector(1,2)
# eval("max(4,5)", {'__builtins__': formula_builtins, {'x' : 1, 'v' : NoteVector(1,2) }, {})
2021-07-09 20:05:04 +02:00
def eval_user_expression(expression, variables):
"""Evalue l'expression (formule utilisateur) avec les variables (dict) données."""
2020-09-26 16:19:37 +02:00
variables["__builtins__"] = formula_builtins
# log('Evaluating %s with %s' % (expression, variables))
# may raise exception if user expression is invalid
return eval(expression, variables, {}) # this should be safe