# -*- coding: utf-8 -*-
# Mathmaker creates automatically maths exercises sheets with their answers
# Copyright 2006-2017 Nicolas Hainaux <nh.techn@gmail.com>
# This file is part of Mathmaker.
# Mathmaker 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 3 of the License, or
# any later version.
# Mathmaker 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 Mathmaker; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
# ------------------------------------------------------------------------------
# --------------------------------------------------------------------------
# ------------------------------------------------------------------------------
##
# @package core.root_calculus
# @brief Mostly abstract classes for mathematical calculus objects.
import copy
import locale
from decimal import (Decimal, getcontext, Rounded, ROUND_DOWN,
InvalidOperation)
from abc import ABCMeta, abstractmethod
from mathmakerlib import required
from mathmakerlib.calculus import Number
from mathmaker.lib.core.utils import check_lexicon_for_substitution
from mathmaker.lib.constants.numeration \
import (UNIT, TENTH, HUNDREDTH, THOUSANDTH, TEN_THOUSANDTH, PRECISION,
PRECISION_REVERSED)
from mathmaker.lib.constants.units import VALUE_AND_UNIT_SEPARATOR
from mathmaker.lib.core.base import Printable
from mathmaker.lib.constants.latex import MARKUP
[docs]class Substitutable(object, metaclass=ABCMeta):
"""
Any object whose (literal) value(s) can be substituted by numeric ones.
Any Substitutable must define a content property, should include an
optional subst_dict argument in its __init__() method and must ensure
that a _subst_dict is defined (an easy way to do this is calling
Substitutable.__init__(self, subst_dict=subst_dict).
The substitute() method is redefined by some Substitutable objects.
"""
def __init__(self, subst_dict=None):
self._subst_dict = None
if subst_dict is not None:
self.subst_dict = subst_dict
@property
@abstractmethod
def content(self):
"""The content to be substituted (list containing literal objects)."""
pass
@property
def subst_dict(self):
"""Get the default dictionary to use for substitution."""
return self._subst_dict
@subst_dict.setter
def subst_dict(self, arg):
"""Set the default dictionary to use for substitution."""
if not isinstance(arg, dict):
raise TypeError('arg should be a dictionnary')
if not check_lexicon_for_substitution(self.content, arg,
'at_least_one'):
raise ValueError('dictionary arg should match the literals '
'of the objects list')
self._subst_dict = arg
[docs] def substitute(self, subst_dict=None):
"""
If a subst_dict has been defined, it is used for literals substitution.
"""
d = self.subst_dict
if subst_dict is not None:
d = subst_dict
if d is not None:
for elt in self.content:
elt.substitute(d)
return self
else:
raise RuntimeError('No dictionary has been provided '
'to perform substitution')
# ------------------------------------------------------------------------------
# --------------------------------------------------------------------------
# ------------------------------------------------------------------------------
##
# @class Evaluable
# @brief Abstract mother class of all (evaluable) mathematical objects
# It is not possible to implement any Evaluable object
[docs]class Evaluable(Printable):
# --------------------------------------------------------------------------
##
# @brief If the object is literal, returns the first letter
# The first term of a Sum, the first factor of a Product etc.
[docs] def get_first_letter(self):
raise NotImplementedError
# --------------------------------------------------------------------------
##
# @brief Returns the numeric value of the object
[docs] def evaluate(self, **options):
raise NotImplementedError
# --------------------------------------------------------------------------
##
# @brief To check if this contains a rounded number...
# @return True or False
[docs] def contains_a_rounded_number(self):
raise NotImplementedError
# --------------------------------------------------------------------------
##
# @brief True if the object contains exactly the given objct
# It can be used to detect objects embedded in a Sum or a Product that
# contain only one term (or factor)
# @param objct The object to search for
# @return True if the object contains exactly the given objct
[docs] def contains_exactly(self, objct):
raise NotImplementedError
# --------------------------------------------------------------------------
##
# @brief Sort order: numerics < sorted literals
# @return -1, 0 or +1
[docs] def alphabetical_order_cmp(self, other_objct):
if self.is_numeric() and other_objct.is_numeric():
return 0
elif self.is_literal() and other_objct.is_numeric():
return 1
elif self.is_numeric() and other_objct.is_literal():
return -1
elif self.is_literal() and other_objct.is_literal():
self_value = self.get_first_letter()
other_value = other_objct.get_first_letter()
# let's compare
if self_value == other_value:
return 0
elif self_value > other_value:
return 1
else:
return -1
# --------------------------------------------------------------------------
##
# @brief True if the object only contains numeric objects
[docs] def is_numeric(self, displ_as=False):
raise NotImplementedError
# --------------------------------------------------------------------------
##
# @brief True if the object only contains literal objects
[docs] def is_literal(self):
raise NotImplementedError
# --------------------------------------------------------------------------
##
# @brief True if the evaluated value of an object is null
[docs] def is_null(self):
raise NotImplementedError
# ------------------------------------------------------------------------------
# --------------------------------------------------------------------------
# ------------------------------------------------------------------------------
##
# @class Calculable
# @brief Abstract mother class of all (calculable) mathematical objects
# It is not possible to implement any Calculable object
[docs]class Calculable(Evaluable):
# --------------------------------------------------------------------------
##
# @brief Returns the list of elements to iter over
[docs] def get_iteration_list(self):
raise NotImplementedError
# --------------------------------------------------------------------------
##
# @brief Returns the next Calculable object during a numeric calculation
[docs] def calculate_next_step(self, **options):
raise NotImplementedError
# --------------------------------------------------------------------------
##
# @brief Returns the next step of expansion/reduction of the Sum
# So, either the Sum of its expanded/reduced terms,
# or the Sum itself reduced, or None
# @return Exponented
[docs] def expand_and_reduce_next_step(self, **options):
raise NotImplementedError
# --------------------------------------------------------------------------
##
# @brief Returns the number of elements of the Exponented
def __len__(self):
raise NotImplementedError
# --------------------------------------------------------------------------
##
# @brief This will iter over the content of the Calculable
def __iter__(self):
return iter(self.get_iteration_list())
def __next__(self):
return next(self.get_iteration_list())
# --------------------------------------------------------------------------
##
# @brief True if the usual writing rules require a × between two factors
# @param objct The other one
# @param position The position (integer) of self in the Product
# @return True if the writing rules require × between self & obj
[docs] def multiply_symbol_is_required(self, objct, position):
raise NotImplementedError
# --------------------------------------------------------------------------
##
# @brief True if the argument requires brackets in a product
# For instance, a Sum with several terms or a negative Item
# @param position The position of the object in the Product
# @return True if the object requires brackets in a Product
[docs] def requires_brackets(self, position):
raise NotImplementedError
# --------------------------------------------------------------------------
##
# @brief True if the argument requires inner brackets
# The reason for requiring them is having an exponent different
# from 1 and several terms or factors (in the case of Products & Sums)
# @return True if the object requires inner brackets
[docs] def requires_inner_brackets(self):
raise NotImplementedError
# --------------------------------------------------------------------------
##
# @brief Uses the given lexicon to substitute literal Values in self
[docs] def substitute(self, subst_dict):
substituted = False
for elt in self:
if elt.substitute(subst_dict):
substituted = True
return substituted
# --------------------------------------------------------------------------
##
# @brief True if the object can be displayed as a single 1
# For instance, the Product 1×1×1×1 or the Sum 0 + 0 + 1 + 0
[docs] def is_displ_as_a_single_1(self):
raise NotImplementedError
# --------------------------------------------------------------------------
##
# @brief True if the object can be displayed as a single int
[docs] def is_displ_as_a_single_int(self):
raise NotImplementedError
# --------------------------------------------------------------------------
##
# @brief True if the object can be displayed as a single -1
# For instance, the Product 1×1×(-1)×1 or the Sum 0 + 0 - 1 + 0
[docs] def is_displ_as_a_single_minus_1(self):
raise NotImplementedError
# --------------------------------------------------------------------------
##
# @brief True if the object can be displayed as a single 0
# For instance, the Product 0×0×0×0 (but not 0×1)
# or the Sum 0 + 0 + 0 (but not 0 + 1 - 1)
[docs] def is_displ_as_a_single_0(self):
raise NotImplementedError
# --------------------------------------------------------------------------
##
# @brief True if the object is or only contains one numeric Item
[docs] def is_displ_as_a_single_numeric_Item(self):
raise NotImplementedError
# --------------------------------------------------------------------------
##
# @brief True if the object can be considered as a neutral element
[docs] def is_displ_as_a_single_neutral(self, elt):
raise NotImplementedError
# ------------------------------------------------------------------------------
# --------------------------------------------------------------------------
# ------------------------------------------------------------------------------
##
# @class Signed
# @brief Signed objects: CommutativeOperations (Sums&Products), Items,
# Quotients...
# Any Signed must have a sign field
[docs]class Signed(Calculable):
# --------------------------------------------------------------------------
##
# @brief Constructor
# @return A Signed, though it can't really be used as is
def __init__(self):
self._sign = '+'
# --------------------------------------------------------------------------
##
# @brief Returns the number of minus signs in the object
[docs] def get_minus_signs_nb(self):
raise NotImplementedError
# --------------------------------------------------------------------------
##
# @brief Returns the sign of the object
[docs] def get_sign(self):
return self._sign
# --------------------------------------------------------------------------
sign = property(get_sign,
doc="Sign of the object")
# --------------------------------------------------------------------------
##
# @brief Set the sign of the object
# @param arg String being '+' or '-' or number being +1 or -1
# @warning Relays an exception if arg is not of the types described
[docs] def set_sign(self, arg):
if arg in ['+', '-']:
self._sign = arg
elif arg == 1:
self._sign = '+'
elif arg == -1:
self._sign = '-'
elif isinstance(arg, Calculable):
if arg.is_displ_as_a_single_1():
self._sign = '+'
elif arg.is_displ_as_a_single_minus_1():
self._sign = '-'
else:
raise TypeError('Got: ' + str(type(arg))
+ ' instead of \'+\' or \'-\' or 1 or -1')
# --------------------------------------------------------------------------
##
# @brief Changes the sign of the object
[docs] def set_opposite_sign(self):
if self.get_sign() == '-':
self.set_sign('+')
elif self.get_sign() == '+':
self.set_sign('-')
else:
# this case should never happen, just to secure the code
raise ValueError('The sign of the object ' + repr(self)
+ ' is ' + str(self.sign)
+ " instead of '+' or '-'.")
# --------------------------------------------------------------------------
##
# @brief True if object's *sign* is '-' (ie -(-1) would be "negative")
[docs] def is_negative(self):
return self.sign == '-'
# --------------------------------------------------------------------------
##
# @brief True if object's *sign* is '+'
[docs] def is_positive(self):
return self.sign == '+'
# ------------------------------------------------------------------------------
# --------------------------------------------------------------------------
# ------------------------------------------------------------------------------
##
# @class Value
# @brief This class embedds Numbers & Strings into a basic object. It doesn't
# have any exponent field (always set to 1), so does not belong to
# Exponenteds. This is the only place where numbers are used directly.
# The Item class for instance, contains Values in its fields, not
# numbers.
# This to be sure any content of any field (even if only a simple
# number is to be saved in the field) can be tested & managed
# as an object in any other class than Value.
# Up from 2010/11/19, it is decided that all numeric Values will contain
# a Decimal.decimal number.
[docs]class Value(Signed):
# --------------------------------------------------------------------------
##
# @brief Constructor
# @param arg Number|String
# If the argument is not of one of these kinds, an exception
# will be raised.
# @return One instance of Value
def __init__(self, arg, text_in_maths=True, **options):
Signed.__init__(self)
self._has_been_rounded = False
self._unit = ""
self._text_in_maths = text_in_maths
if 'unit' in options:
self._unit = Unit(options['unit'])
if any([isinstance(arg, c) for c in [float, int, Decimal]]):
self._raw_value = Decimal(str(arg))
if arg >= 0:
self._sign = '+'
else:
self._sign = '-'
elif type(arg) == str:
try:
self._raw_value = Decimal(arg)
except InvalidOperation:
self._raw_value = arg
if len(arg) >= 1 and arg[0] == '-':
self._sign = '-'
elif isinstance(arg, Value):
self._raw_value = arg.raw_value
self._has_been_rounded = arg.has_been_rounded
self._unit = arg.unit
self._sign = arg.sign
# All other unforeseen cases: an exception is raised.
else:
raise TypeError('Got: ' + str(type(arg))
+ ' instead of Number|String')
if self._sign == '-':
if isinstance(self._raw_value, str):
self._abs_value = self._raw_value[1:]
else:
self._abs_value = - self._raw_value
else:
self._abs_value = self._raw_value
# --------------------------------------------------------------------------
##
# @brief If the object is literal, returns the value
[docs] def get_first_letter(self):
if self.is_literal():
return self.raw_value
else:
raise TypeError('Cannot get first letter of a non literal Value')
# --------------------------------------------------------------------------
##
# @brief Returns the "has been rounded" state of the Value
[docs] def get_has_been_rounded(self):
return self._has_been_rounded
# --------------------------------------------------------------------------
##
# @brief Returns the list of elements to iter over
[docs] def get_iteration_list(self):
return [self.raw_value]
@property
def raw_value(self):
return self._raw_value
@raw_value.setter
def raw_value(self, arg):
if any([isinstance(arg, c) for c in [float, int, Decimal]]):
# __
self._raw_value = Decimal(str(arg))
if arg >= 0:
self._sign = '+'
else:
self._sign = '-'
elif isinstance(arg, str):
try:
self._raw_value = Decimal(arg)
except InvalidOperation:
self._raw_value = arg
if len(arg) >= 1 and arg[0] == '-':
self._sign = '-'
if self._sign == '-':
if isinstance(self._raw_value, str):
self._abs_value = self._raw_value[1:]
else:
self._abs_value = - self._raw_value
else:
self._abs_value = self._raw_value
# --------------------------------------------------------------------------
##
# @brief Returns the sign of the Value
[docs] def get_sign(self):
return self._sign
# --------------------------------------------------------------------------
##
# @brief Returns the unit of the Value
[docs] def get_unit(self):
return self._unit
@property
def abs_value(self):
return self._abs_value
has_been_rounded = property(get_has_been_rounded,
doc="'has been rounded' state of the Value")
sign = property(get_sign,
doc="Sign of the Value")
unit = property(get_unit,
doc="Unit of the Value")
# --------------------------------------------------------------------------
##
# @brief Sets the "has been rounded" state of the Value
[docs] def set_has_been_rounded(self, arg):
if arg not in [True, False]:
raise ValueError('Got: ' + str(type(arg))
+ ' instead of True|False')
else:
self._has_been_rounded = arg
# --------------------------------------------------------------------------
##
# @brief Set the sign of the object
# @param arg String being '+' or '-' or number being +1 or -1
# @warning Relays an exception if arg is not of the types described
[docs] def set_sign(self, arg):
if arg in ['+', '-']:
self._sign = arg
elif arg == 1:
self._sign = '+'
elif arg == -1:
self._sign = '-'
elif isinstance(arg, Calculable):
if arg.is_displ_as_a_single_1():
self._sign = '+'
elif arg.is_displ_as_a_single_minus_1():
self._sign = '-'
else:
raise TypeError('Got: ' + str(type(arg))
+ ' instead of \'+\' or \'-\' or 1 or -1')
# --------------------------------------------------------------------------
##
# @brief Set the unit of the Value
# @param arg String
[docs] def set_unit(self, arg):
self._unit = Unit(arg)
# --------------------------------------------------------------------------
##
# @brief Changes the sign of the object
[docs] def set_opposite_sign(self):
if self.get_sign() == '-':
self.set_sign('+')
elif self.get_sign() == '+':
self.set_sign('-')
else:
# this case should never happen, just to secure the code
raise ValueError('The sign of the object ' + repr(self)
+ ' is ' + str(self.sign)
+ " instead of '+' or '-'.")
# --------------------------------------------------------------------------
##
# @brief Temporary shortcut for into_str()
def __str__(self, **options):
return self.into_str(**options)
# --------------------------------------------------------------------------
##
# @brief Creates a string of the given object in the given ML
# @param options Any options
# @return The formated string
[docs] def into_str(self, textwrap=True, js_repr=False, **options):
sign = ''
if self._sign == '-':
sign = '-'
open_text_in_maths = MARKUP['open_text_in_maths'] \
if self._text_in_maths \
else ''
close_text_in_maths = MARKUP['close_text_in_maths'] \
if self._text_in_maths \
else ''
if js_repr or not textwrap:
open_text_in_maths = close_text_in_maths = ''
if open_text_in_maths:
required.package['amsmath'] = True
options.update({'textwrap': textwrap, 'js_repr': js_repr})
if self.is_numeric():
lvalue = locale.str(self.abs_value)
if options.get('js_repr'):
lvalue = str(self.abs_value)
if 'display_unit' in options and options['display_unit']:
unit_str = self.unit.into_str(**options) \
if isinstance(self.unit, Unit) \
else str(self.unit)
return sign + open_text_in_maths + lvalue \
+ close_text_in_maths + unit_str
elif 'display_SI_unit' in options and options['display_SI_unit']:
unit_str = self.unit.into_str(**options) \
if isinstance(self.unit, Unit) \
else str(self.unit)
return sign + "\SI{" + lvalue + "}{" + unit_str + "}"
else:
return sign + open_text_in_maths + lvalue + close_text_in_maths
elif (self.raw_value in ["", " "] and 'display_SI_unit' in options
and options['display_SI_unit']):
# __
unit_str = self.unit.into_str(**options) \
if isinstance(self.unit, Unit) \
else str(self.unit)
return sign + "\SI{" + self.abs_value + "}"\
"{" + unit_str + "}"
elif ('?' in self.raw_value and 'display_unit' in options
and options['display_unit']):
# __
unit_str = self.unit.into_str(**options) \
if isinstance(self.unit, Unit) \
else str(self.unit)
return sign + open_text_in_maths + self.abs_value \
+ close_text_in_maths + unit_str
else: # self.is_literal()
if (len(self.get_first_letter()) >= 2
and not self.get_first_letter()[0] in ["-", "+"]):
# __
return sign + open_text_in_maths \
+ str(self.abs_value) \
+ close_text_in_maths
else:
return str(self.raw_value)
# --------------------------------------------------------------------------
##
# @brief Returns the value of a numeric Value
# @warning Raise an exception if not numeric
[docs] def evaluate(self, **options):
if not self.is_numeric():
raise TypeError('Cannot evaluate a non numeric Value')
else:
return self.raw_value
# --------------------------------------------------------------------------
##
# @brief Returns None
[docs] def calculate_next_step(self, **options):
return None
# --------------------------------------------------------------------------
##
# @brief Debugging method to print the Value
def __repr__(self, **options):
return "." + str(self.raw_value) + "."
# --------------------------------------------------------------------------
##
# @brief Compares two Values
# @todo check if __eq__ shouldn't return +1 if value of self > objct
# @todo comparison directly with numbers... (see alphabetical_order_cmp)
# @return True if they're equal
def __eq__(self, other_value):
if not isinstance(other_value, Value):
return False
if self.raw_value == other_value.raw_value:
return True
else:
return False
def __ne__(self, other_value):
"""Basically the contrary of __eq__, to allow comparisons with !="""
return not self.__eq__(other_value)
# --------------------------------------------------------------------------
##
# @brief Returns the Value's length
# @return 1
def __len__(self):
return 1
# --------------------------------------------------------------------------
##
# @brief Makes Values hashable (so, usable as dictionnary keys)
def __hash__(self):
return hash(str(self.sign) + str(self.raw_value)
+ str(self.has_been_rounded) + str(self.unit))
# --------------------------------------------------------------------------
##
# @brief Executes the multiplication with another object
# @warning Will raise an error if you try to multiply a literal
# with a number
def __mul__(self, objct):
if isinstance(objct, Calculable):
return self.raw_value * objct.evaluate()
else:
return self.raw_value * objct
# --------------------------------------------------------------------------
##
# @brief Executes the addition with another object
# @warning Will raise an error if you try to add a literal with a number
def __add__(self, objct):
if isinstance(objct, Calculable):
return self.raw_value + objct.evaluate()
else:
return self.raw_value + objct
def __sub__(self, v):
"""
Return the difference between self and another Value, as a Value.
:param v: the Value to substract of self
:type v: Value
:rtype: Value
"""
if isinstance(v, Value):
return Value(self.raw_value - v.raw_value)
else:
raise TypeError('TypeError: unsupported operand type(s) for -:'
' \'Value\' and \'{}\''
.format(str(type(v))))
# --------------------------------------------------------------------------
##
# @brief Uses the given lexicon to substitute literal Values in self
[docs] def substitute(self, subst_dict):
substituted = False
if self.is_literal():
for key in subst_dict:
if self == key:
self.__init__(subst_dict[key])
substituted = True
else:
pass
return substituted
# --------------------------------------------------------------------------
##
# @brief Returns a Value containing the square root of self
[docs] def sqrt(self):
if self.is_numeric():
return Value(self.raw_value.sqrt())
else:
raise TypeError('Cannot calculate the square root of a non '
'numeric Value')
# --------------------------------------------------------------------------
##
# @brief Returns the value once rounded to the given precision
[docs] def rounded(self, precision):
if not self.is_numeric():
raise TypeError('Cannot round a non numeric Value')
elif (not (precision in [UNIT, TENTH, HUNDREDTH, THOUSANDTH,
TEN_THOUSANDTH]
or (type(precision) == int and 0 <= precision <= 4))):
# __
raise TypeError('Got: ' + str(type(precision))
+ ' instead of UNIT or TENTH, HUNDREDTH, '
'THOUSANDTH, TEN_THOUSANDTH, or 0, 1, 2, 3 or 4.')
else:
result_value = self.clone()
if type(precision) == int:
result_value.raw_value = Number(self.raw_value)\
.rounded(Decimal(PRECISION[precision]))
else:
result_value.raw_value = Number(self.raw_value)\
.rounded(Decimal(precision))
if self.needs_to_get_rounded(precision):
result_value.set_has_been_rounded(True)
return result_value
# --------------------------------------------------------------------------
##
# @brief Returns the number of digits of a numerical value
[docs] def digits_number(self):
if not self.is_numeric():
raise TypeError('Cannot get the number of digits of a non '
'numeric Value')
else:
temp_result = len(str((self.raw_value
- Number(self.raw_value)
.rounded(Decimal(UNIT),
rounding=ROUND_DOWN)))) - 2
if temp_result < 0:
return 0
else:
return temp_result
# --------------------------------------------------------------------------
##
# @brief Returns True/False depending on the need of the value to get
# rounded (for instance 2.68 doesn't need to get rounded if
# precision is HUNDREDTH or more, but needs it if it is less)
[docs] def needs_to_get_rounded(self, precision):
if (not (precision in [UNIT, TENTH, HUNDREDTH, THOUSANDTH,
TEN_THOUSANDTH]
or (type(precision) == int and 0 <= precision <= 4))):
# __
raise TypeError('Got: ' + str(type(precision))
+ ' instead of UNIT or TENTH, HUNDREDTH, '
'THOUSANDTH, TEN_THOUSANDTH, or 0, 1, 2, 3 or 4.')
precision_to_test = 0
if type(precision) == int:
precision_to_test = precision
else:
precision_to_test = PRECISION_REVERSED[precision]
return self.digits_number() > precision_to_test
# --------------------------------------------------------------------------
##
# @brief To check if this contains a rounded number...
# @return True or False depending on the Value inside
[docs] def contains_a_rounded_number(self):
return self.has_been_rounded
# --------------------------------------------------------------------------
##
# @brief Always False for a Value
# @param objct The object to search for
# @return False
[docs] def contains_exactly(self, objct):
return False
# --------------------------------------------------------------------------
##
# @brief True if the object contains a perfect square
# (integer or decimal)
[docs] def is_a_perfect_square(self):
if not self.is_numeric():
raise TypeError('Cannot tell if a non numeric Value is a perfect '
'square')
if self.is_an_integer():
return not self.sqrt().needs_to_get_rounded(0)
else:
return len(str(self.raw_value)) > len(str(self.raw_value.sqrt()))
# --------------------------------------------------------------------------
##
# @brief True if the object contains an integer (numeric)
[docs] def is_an_integer(self):
if not self.is_numeric():
raise TypeError('Cannot tell if a non numeric Value is an integer')
getcontext().clear_flags()
self.raw_value.to_integral_exact()
return getcontext().flags[Rounded] == 0
# --------------------------------------------------------------------------
##
# @brief True if the object only contains numeric objects
[docs] def is_numeric(self, displ_as=False):
return any([isinstance(self.raw_value, c)
for c in [float, int, Decimal]])
# --------------------------------------------------------------------------
##
# @brief True if the object only contains literal objects
[docs] def is_literal(self, displ_as=False) -> bool:
"""
Return True if Value is to be considered literal.
:param displ_as: not applicable to Values
"""
if type(self.raw_value) == str:
# __
return True
else:
return False
# --------------------------------------------------------------------------
##
# @brief True if the evaluated value of an object is null
[docs] def is_null(self):
if self.is_numeric() and self.raw_value == 0:
return True
else:
return False
# --------------------------------------------------------------------------
##
# @brief True if the object can be displayed as a single 1
[docs] def is_displ_as_a_single_1(self):
if self.is_numeric() and self.raw_value == 1:
return True
else:
return False
# --------------------------------------------------------------------------
##
# @brief True if the object can be displayed as a single -1
[docs] def is_displ_as_a_single_minus_1(self):
if self.is_numeric() and self.raw_value == -1:
return True
else:
return False
# --------------------------------------------------------------------------
##
# @brief True if the object can be displayed as a single 0
[docs] def is_displ_as_a_single_0(self):
if self.is_numeric() and self.raw_value == 0:
return True
else:
return False
# --------------------------------------------------------------------------
##
# @brief True if the object is or only contains one numeric Item
[docs] def is_displ_as_a_single_numeric_Item(self):
return False
# --------------------------------------------------------------------------
##
# @brief True if the object can be displayed as a single int
[docs] def is_displ_as_a_single_int(self):
return self.is_numeric() and self.is_an_integer()
# ------------------------------------------------------------------------------
# --------------------------------------------------------------------------
# ------------------------------------------------------------------------------
##
# @class Exponented
# @brief Exponented objects: CommutativeOperations (Sums&Products), Items,
# Quotients...
# Any Exponented must have a exponent field and should reimplement the
# methods that are not already defined hereafter
[docs]class Exponented(Signed, metaclass=ABCMeta):
# --------------------------------------------------------------------------
##
# @brief Constructor
# @return An Exponented, though it can't really be used as is
def __init__(self):
Signed.__init__(self)
self._exponent = Value(1)
# --------------------------------------------------------------------------
##
# @brief Gets the exponent of the Function
# @brief this should be already done by Item.get_exponent()...
[docs] def get_exponent(self):
return self._exponent
# --------------------------------------------------------------------------
exponent = property(get_exponent, doc="Exponent of the Function")
# --------------------------------------------------------------------------
##
# @brief Set the value of the exponent
# @param arg Calculable|Number|String
# @warning Relays an exception if arg is not of the types described
[docs] def set_exponent(self, arg):
if isinstance(arg, Calculable):
self._exponent = arg.clone()
else:
self._exponent = Value(arg)
# --------------------------------------------------------------------------
##
# @brief True if the exponent isn't equivalent to a single 1
# @return True if the exponent is not equivalent to a single 1
[docs] def exponent_must_be_displayed(self):
if not self.exponent.is_displ_as_a_single_1():
return True
else:
return False
# ------------------------------------------------------------------------------
# --------------------------------------------------------------------------
# ------------------------------------------------------------------------------
##
# @class Unit
# @brief This class is used to handle with units.
[docs]class Unit(Exponented):
# --------------------------------------------------------------------------
##
# @brief Constructor
# @return One instance of Unit
def __init__(self, arg, **options):
Exponented.__init__(self)
if type(arg) == str:
self._name = arg
if 'exponent' in options:
self._exponent = Value(options['exponent'])
elif type(arg) == Unit:
self._name = copy.deepcopy(arg.name)
self._exponent = copy.deepcopy(arg.exponent)
else:
raise ValueError('arg should be a string or a Unit')
@property
def name(self):
return self._name
@property
def exponent(self):
return self._exponent
# --------------------------------------------------------------------------
##
# @brief Creates a string of the given object in the given ML
# @param options Any options
# @return The formated string
[docs] def into_str(self, textwrap=True, js_repr=False, **options):
text_box_open = MARKUP['open_text_in_maths']
text_box_close = MARKUP['close_text_in_maths']
if (not textwrap) or ('display_SI_unit' in options) or js_repr:
text_box_open = text_box_close = ''
exponent = ''
if text_box_open:
required.package['amsmath'] = True
if self.exponent != Value(1):
exponent = MARKUP['opening_exponent'] \
+ str(self.exponent) \
+ MARKUP['closing_exponent']
separator = VALUE_AND_UNIT_SEPARATOR[self.name] \
if 'display_SI_unit' not in options \
else ""
return separator + text_box_open + self.name \
+ text_box_close + exponent
def __repr__(self):
"""Raw representation of a Unit object"""
expo_str = ''
if not self.exponent.is_displ_as_a_single_1():
expo_str = '^' + repr(self.exponent)
return self._name + expo_str