diff --git a/.data/history.txt b/.data/history.txt index 135b662..d4d5973 100644 --- a/.data/history.txt +++ b/.data/history.txt @@ -1,5 +1,41 @@ -valor = 500 -k = valor * symbols('Hz') / symbols('mmS') +# Italiano +faltan_italiano = 51 +hechas_italiano = 21 +total_italiano = faltan_italiano+hechas_italiano -valor_mmS = 100 * symbols('mmS') -speed = k * valor_mmS \ No newline at end of file +# Matematica +faltan_mate = 59 +hechas_mate = 6 +total_mate = faltan_mate+hechas_mate + +# Matematica Libro +faltan_mate2 = 25 +hechas_mate2 = 0 +total__mate2 = hechas_mate2+faltan_mate2 + +# Ingles +faltan_english = 48 +hechas_english = 3 +total__english = hechas_english + faltan_english + +# Resumen +total = total_italiano+total_mate+total__mate2+total__english +hecho = hechas_italiano + hechas_mate + hechas_mate2 + hechas_english +faltan = total - hecho + +# Estado actual de dias +dias_pasados = 28-6 +dias_faltan = 31+31+14+2 + +# Promedio actual +hecho/dias_pasados + +# Minimo para terminar +hacer_por_dia = faltan/dias_faltan + +# Minimo para terminar +promedio_ideal = total/(dias_pasados+dias_faltan) + + +Date('23/4/25')=Date('23/2/25') + x +x=? diff --git a/app/evaluation.py b/app/evaluation.py index 8a48163..4d53f65 100644 --- a/app/evaluation.py +++ b/app/evaluation.py @@ -407,6 +407,13 @@ class PureAlgebraicEngine: eval_context = self._get_complete_context() result_obj = sympify(expression_str, locals=eval_context) + # NUEVA FUNCIONALIDAD: Aplicar simplificación simbólica automática en asignaciones + if hasattr(result_obj, 'free_symbols') and result_obj.free_symbols and self.equations: + simplified_obj = self._apply_symbolic_simplification(result_obj) + if simplified_obj != result_obj: + result_obj = simplified_obj + self.logger.debug(f"Simplificación simbólica aplicada en asignación: {expression_str} → {result_obj}") + # 1. ASIGNACIÓN AL CONTEXTO (para evaluación directa) self.symbol_table[var_name] = result_obj self.variables.add(var_name) @@ -509,6 +516,13 @@ class PureAlgebraicEngine: eval_context = self._get_complete_context() result_obj = sympify(line, locals=eval_context) + # NUEVA FUNCIONALIDAD: Aplicar simplificación simbólica automática + if hasattr(result_obj, 'free_symbols') and result_obj.free_symbols and self.equations: + simplified_obj = self._apply_symbolic_simplification(result_obj) + if simplified_obj != result_obj: + result_obj = simplified_obj + self.logger.debug(f"Simplificación simbólica aplicada: {line} → {result_obj}") + # Si es un PlotResult, asegurar que tenga la línea original if isinstance(result_obj, PlotResult) and not result_obj.original_expression: result_obj.original_expression = line @@ -600,15 +614,33 @@ class PureAlgebraicEngine: # Verificar que el resultado no sea problemático if final_value != var_symbol and not isinstance(final_value, (sp.logic.boolalg.BooleanTrue, sp.logic.boolalg.BooleanFalse)): + # NUEVA FUNCIONALIDAD: Aplicar simplificación simbólica al resultado final + if hasattr(final_value, 'free_symbols') and final_value.free_symbols and self.equations: + simplified_final = self._apply_symbolic_simplification(final_value) + if simplified_final != final_value: + self.logger.debug(f"Simplificación aplicada en resultado final: {final_value} → {simplified_final}") + final_value = simplified_final return Eq(var_symbol, final_value) else: # Si hay problema con la resolución, devolver simbólico + # Aplicar simplificación también al valor simbólico + if hasattr(solution_value, 'free_symbols') and solution_value.free_symbols and self.equations: + simplified_solution = self._apply_symbolic_simplification(solution_value) + if simplified_solution != solution_value: + self.logger.debug(f"Simplificación aplicada en solución simbólica: {solution_value} → {simplified_solution}") + solution_value = simplified_solution return Eq(var_symbol, solution_value) else: # Hay variables con valores simbólicos, devolver ecuación simbólica return Eq(var_symbol, solution_value) else: # No hay variables sin resolver, es ya un valor final + # NUEVA FUNCIONALIDAD: Aplicar simplificación simbólica al valor final + if hasattr(solution_value, 'free_symbols') and solution_value.free_symbols and self.equations: + simplified_solution = self._apply_symbolic_simplification(solution_value) + if simplified_solution != solution_value: + self.logger.debug(f"Simplificación aplicada en valor final: {solution_value} → {simplified_solution}") + solution_value = simplified_solution return Eq(var_symbol, solution_value) else: # Si no hay solución en las ecuaciones, verificar en symbol_table @@ -617,6 +649,12 @@ class PureAlgebraicEngine: value = self.symbol_table[var_name] # Si el valor en symbol_table es diferente de la variable, devolverlo if value != var_symbol: + # NUEVA FUNCIONALIDAD: Aplicar simplificación simbólica también aquí + if hasattr(value, 'free_symbols') and value.free_symbols and self.equations: + simplified_value = self._apply_symbolic_simplification(value) + if simplified_value != value: + self.logger.debug(f"Simplificación aplicada en solve: {value} → {simplified_value}") + value = simplified_value return Eq(var_symbol, value) # Si no hay información, devolver ecuación con la variable sin resolver @@ -715,6 +753,85 @@ class PureAlgebraicEngine: self.logger.debug(f"Error resolviendo {var_symbol}: {e}") return var_symbol + def _apply_symbolic_simplification(self, expression): + """ + Aplica simplificación simbólica automática usando ecuaciones definidas + + Esta función sustituye símbolos por sus definiciones de ecuaciones cuando es posible, + permitiendo simplificaciones como: mS = mmS/1000, entonces 50000*Hz*mS/mmS → 50*Hz + """ + try: + current_expr = expression + max_iterations = 5 # Evitar bucles infinitos + + for iteration in range(max_iterations): + substituted = current_expr + substitution_made = False + + # Obtener símbolos libres de la expresión actual + free_symbols = substituted.free_symbols + + # Para cada símbolo en la expresión, buscar si hay una ecuación que lo defina + for symbol in free_symbols: + symbol_definition = self._find_symbol_definition(symbol) + + if symbol_definition is not None and symbol_definition != symbol: + # Aplicar la sustitución + new_expr = substituted.subs(symbol, symbol_definition) + + # Simplificar después de la sustitución + simplified = sp.simplify(new_expr) + + if simplified != substituted: + substituted = simplified + substitution_made = True + self.logger.debug(f"Símbolo {symbol} sustituido por {symbol_definition}") + + # Si no se hicieron sustituciones, terminar + if not substitution_made: + break + + current_expr = substituted + + return current_expr + + except Exception as e: + self.logger.debug(f"Error en simplificación simbólica: {e}") + return expression + + def _find_symbol_definition(self, symbol): + """ + Busca la definición de un símbolo en las ecuaciones del sistema + + Returns: + La expresión que define al símbolo, o None si no se encuentra + """ + try: + # Buscar en ecuaciones donde el símbolo aparece solo en el lado izquierdo + for eq in self.equations: + if hasattr(eq, 'lhs') and hasattr(eq, 'rhs'): + # Caso 1: símbolo = expresión + if eq.lhs == symbol: + return eq.rhs + + # Caso 2: expresión = símbolo + elif eq.rhs == symbol: + return eq.lhs + + # Buscar en symbol_table como respaldo + symbol_name = str(symbol) + if symbol_name in self.symbol_table: + value = self.symbol_table[symbol_name] + # Solo usar si el valor es diferente del símbolo mismo + if value != symbol: + return value + + return None + + except Exception as e: + self.logger.debug(f"Error buscando definición de {symbol}: {e}") + return None + def _resolve_iteratively(self, expression, max_iterations=10): """Resuelve una expresión iterativamente sustituyendo valores conocidos""" try: diff --git a/app/gui_main.py b/app/gui_main.py index c025385..166b7e1 100644 --- a/app/gui_main.py +++ b/app/gui_main.py @@ -315,10 +315,21 @@ class HybridCalculatorPySide6(QMainWindow): self.latex_button.setChecked(True) self.latex_panel_visible = True - # Configurar tamaños + # Configurar tamaños con límites latex_width = self.settings.get("latex_panel_width", 300) total_width = self.width() + + # CORRECCIÓN: Aplicar límites para evitar crecimiento descontrolado + min_latex_width = 200 + max_latex_width = min(600, total_width * 0.5) # Máximo 50% del ancho total + latex_width = max(min_latex_width, min(latex_width, max_latex_width)) + main_width = total_width - latex_width - 50 # 50 para el botón + # Asegurar que main_width no sea negativo + if main_width < 200: + main_width = 200 + latex_width = total_width - main_width - 50 + self.secondary_splitter.setSizes([main_width, latex_width]) # Actualizar panel con ecuaciones pendientes @@ -346,11 +357,17 @@ class HybridCalculatorPySide6(QMainWindow): def closeEvent(self, event): """Maneja el cierre de la aplicación""" - # Guardar ancho del panel LaTeX si está visible + # Guardar ancho del panel LaTeX si está visible con límites de validación if self.latex_panel_visible and self.secondary_splitter.count() > 1: sizes = self.secondary_splitter.sizes() if len(sizes) > 1: latex_width = sizes[1] + + # CORRECCIÓN: Validar y limitar el ancho antes de guardarlo + min_latex_width = 200 + max_latex_width = 600 + latex_width = max(min_latex_width, min(latex_width, max_latex_width)) + self.settings_manager.set_setting("latex_panel_width", latex_width) # Delegar al settings manager diff --git a/custom_types/date_type.py b/custom_types/date_type.py new file mode 100644 index 0000000..0bdaabe --- /dev/null +++ b/custom_types/date_type.py @@ -0,0 +1,374 @@ +""" +Módulo de manejo de fechas para el sistema de tipos +Permite crear y manipular fechas con aritmética de días y semanas, soporte para +operaciones simbólicas a través de SymPy, y utilidades comunes como cálculo de +fines de semana, días hábiles y diferencias entre fechas. + +Ejemplo de uso: + >>> d = Date("10/5/25") # 10 de mayo de 2025 + >>> d + 7 # 17/5/25 + >>> d.day_of_week() # 'Sábado' + >>> d.is_weekend() # True + >>> d.next_business_day() # 12/5/25 +""" + +from __future__ import annotations + +import datetime +from typing import List, Tuple, Optional, Union +import sympy + +from app.class_base import ( + ClassBase, +) # noqa: F401 (aún no se usa, pero se importa para coherencia) +from app.sympy_Base import SympyClassBase + +__all__ = [ + "Class_Date", + "Date", # alias de conveniencia + "Today", # nueva función constructora +] + + +def _parse_date_str(date_str: str) -> datetime.date: + """Convierte una cadena a objeto ``datetime.date``. + + Acepta los formatos comunes: + - DD/MM/YY (se asume 2000-2099 para YY < 70 y 1900-1999 para el resto) + - DD/MM/YYYY + - YYYY-MM-DD (ISO-8601) + + Asimismo se admiten separadores «/», «-» o «.». + """ + date_str = date_str.strip() + + # Intentar ISO-8601 directamente + try: + return datetime.date.fromisoformat(date_str) + except ValueError: + pass + + # Reemplazar posibles separadores por «/» para simplificar + normalised = date_str.replace("-", "/").replace(".", "/") + parts = normalised.split("/") + if len(parts) != 3: + raise ValueError(f"Formato de fecha desconocido: '{date_str}'") + + # Detectar si el primer campo es año (ISO) o día + if len(parts[0]) == 4: # AAAA/MM/DD + year = int(parts[0]) + month = int(parts[1]) + day = int(parts[2]) + else: # DD/MM/AA o DD/MM/YYYY + day = int(parts[0]) + month = int(parts[1]) + year_part = parts[2] + if len(year_part) == 2: + yy = int(year_part) + year = 2000 + yy if yy < 70 else 1900 + yy + else: + year = int(year_part) + return datetime.date(year, month, day) + + +class Class_Date(SympyClassBase): + """Clase de fecha con aritmética de días y semanas. + + Internamente la fecha se almacena como el ordinal devuelto por + :py:meth:`datetime.date.toordinal`, lo que facilita la aritmética con + números enteros (p.ej. añadir días). + """ + + _op_priority = 20 # preferencia alta en operaciones de SymPy + + def __init__(self, value: Union[str, datetime.date, int, sympy.Expr]): + # Manejar expresiones SymPy (p. ej. símbolos o enteros simbólicos) + if isinstance(value, sympy.Expr) and not isinstance(value, sympy.Integer): + # Modo simbólico puro (no intentamos resolver a fecha real) + self._ordinal_expr = value + self._has_symbols = True + date_repr = str(value) + else: + # Convertir todos los casos a «datetime.date» y ordinal entero + if isinstance(value, datetime.date): + date_obj = value + elif isinstance(value, str): + date_obj = _parse_date_str(value) + elif isinstance(value, (int, sympy.Integer)): + # Se interpreta como ordinal absoluto + date_obj = datetime.date.fromordinal(int(value)) + else: + raise TypeError( + "value debe ser str, datetime.date o entero (ordinal), " + f"recibido: {type(value)}" + ) + + self._ordinal = date_obj.toordinal() + self._has_symbols = False + self._date = date_obj + self._ordinal_expr = sympy.Integer(self._ordinal) + date_repr = self._date.isoformat() + + super().__init__(self._ordinal_expr, date_repr) + + # ------------------------------------------------------------------ + # Representación + # ------------------------------------------------------------------ + + def __repr__(self): # noqa: D401 + return f"Date('{self.__str__()}')" + + def __str__(self): # noqa: D401 + if self._has_symbols: + return str(self._ordinal_expr) + return self._date.strftime("%d/%m/%y") + + def _sympystr(self, printer): # pragma: no cover + return str(self) + + # ------------------------------------------------------------------ + # Conversión + # ------------------------------------------------------------------ + + def to_datetime(self) -> datetime.date: + """Devuelve la fecha como objeto ``datetime.date``.""" + if self._has_symbols: + raise ValueError( + "La fecha es simbólica y no se puede convertir a 'datetime.date'." + ) + return self._date + + def to_ordinal(self) -> int: + """Devuelve el ordinal (días desde 01-01-0001).""" + return int(self._ordinal_expr) + + # ------------------------------------------------------------------ + # Aritmética + # ------------------------------------------------------------------ + + def __add__(self, other): + """Añadir días o expresiones simbólicas: ``fecha + n`` o ``fecha + símbolo``.""" + # Soporte para expresiones SymPy (símbolos, enteros simbólicos, etc.) + if isinstance(other, sympy.Expr): + new_expr = self._ordinal_expr + other + return Class_Date(new_expr) + if hasattr(other, "__int__"): + return self.add_days(int(other)) + return NotImplemented + + __radd__ = __add__ + + def __sub__(self, other): + """Resta días o devuelve diferencia: ``fecha - n``, ``fecha - símbolo``, o diferencia entre fechas.""" + if isinstance(other, sympy.Expr): + # Restar expresión simbólica -> nueva fecha simbólica + new_expr = self._ordinal_expr - other + return Class_Date(new_expr) + if hasattr(other, "__int__"): + return self.add_days(-int(other)) + if isinstance(other, Class_Date): + # Diferencia (puede ser simbólica) + return self._ordinal_expr - other._ordinal_expr + return NotImplemented + + def add_days(self, n: int) -> "Class_Date": + """Devuelve una nueva fecha desplazada *n* días.""" + if self._has_symbols: + new_expr = self._ordinal_expr + n + return Class_Date(new_expr) + else: + new_date = self._date + datetime.timedelta(days=n) + return Class_Date(new_date) + + def add_weeks(self, n: int) -> "Class_Date": + """Devuelve una nueva fecha desplazada *n* semanas.""" + return self.add_days(n * 7) + + def next_day(self) -> "Class_Date": + return self.add_days(1) + + def previous_day(self) -> "Class_Date": + return self.add_days(-1) + + # ------------------------------------------------------------------ + # Información de calendario + # ------------------------------------------------------------------ + + _WEEKDAY_NAMES_ES = [ + "Lunes", + "Martes", + "Miércoles", + "Jueves", + "Viernes", + "Sábado", + "Domingo", + ] + + def day_of_week(self) -> str: + """Nombre del día de la semana en español (Lunes-Domingo).""" + if self._has_symbols: + raise ValueError( + "La fecha es simbólica; no se puede determinar el día de la semana." + ) + weekday = self._date.weekday() # 0 = lunes + return self._WEEKDAY_NAMES_ES[weekday] + + def is_weekend(self) -> bool: + if self._has_symbols: + raise ValueError( + "La fecha es simbólica; no se puede determinar si es fin de semana." + ) + return self._date.weekday() >= 5 + + # ------------------------------------------------------------------ + # Días hábiles + # ------------------------------------------------------------------ + + def is_business_day(self, holidays: Optional[List[datetime.date]] = None) -> bool: + """Indica si es día laborable (L-V) y no festivo. + + Args: + holidays: lista opcional de festivos como ``datetime.date``. + """ + if self._has_symbols: + raise ValueError( + "La fecha es simbólica; no se puede evaluar si es laborable." + ) + if self.is_weekend(): + return False + if holidays and self._date in holidays: + return False + return True + + def next_business_day( + self, holidays: Optional[List[datetime.date]] = None + ) -> "Class_Date": + """Devuelve el siguiente día laborable.""" + candidate = self.next_day() + while not candidate.is_business_day(holidays): + candidate = candidate.next_day() + return candidate + + def business_days_between( + self, other: "Class_Date", holidays: Optional[List[datetime.date]] = None + ) -> int: + """Cuenta días laborables entre dos fechas (excluye la inicial, incluye la final si procede).""" + if not isinstance(other, Class_Date): + raise TypeError("other debe ser Class_Date") + start_ord = min(self.to_ordinal(), other.to_ordinal()) + 1 + end_ord = max(self.to_ordinal(), other.to_ordinal()) + count = 0 + for ord_val in range(start_ord, end_ord + 1): + d = Class_Date(ord_val) + if d.is_business_day(holidays): + count += 1 + return count + + # ------------------------------------------------------------------ + # Diferencias + # ------------------------------------------------------------------ + + def days_between(self, other: "Class_Date") -> int: + """Número absoluto de días entre dos fechas.""" + if not isinstance(other, Class_Date): + raise TypeError("other debe ser Class_Date") + return abs(self - other) + + def weeks_between(self, other: "Class_Date") -> float: + """Número de semanas (con decimales) entre dos fechas.""" + return self.days_between(other) / 7 + + # ------------------------------------------------------------------ + # Comparaciones y ecuaciones simbólicas + # ------------------------------------------------------------------ + + def __eq__(self, other): # type: ignore[override] + """Devuelve una ecuación SymPy en vez de comparar booleanamente cuando se usa con objetos Date o expresiones SymPy.""" + if isinstance(other, Class_Date): + return sympy.Eq(self._ordinal_expr, other._ordinal_expr) + if isinstance(other, sympy.Expr): + return sympy.Eq(self._ordinal_expr, other) + return NotImplemented + + # ------------------------------------------------------------------ + # Helper para autocompletado & paneles GUI + # ------------------------------------------------------------------ + + @staticmethod + def Helper(input_str): # pragma: no cover + if input_str.strip().lower().startswith("date("): + return ( + "Ej: date('10/5/25'), date('2025-05-10')\n" # noqa: E501 + "Funciones: add_days(), add_weeks(), next_business_day(), day_of_week(), is_weekend(), is_business_day(), days_between()…" + ) + return None + + @staticmethod + def PopupFunctionList(): # pragma: no cover + return [ + ("add_days", "Suma N días"), + ("add_weeks", "Suma N semanas"), + ("next_day", "Día siguiente"), + ("previous_day", "Día anterior"), + ("next_business_day", "Siguiente día hábil"), + ("is_business_day", "¿Es día laborable?"), + ("day_of_week", "Nombre del día de la semana"), + ("is_weekend", "¿Es fin de semana?"), + ("days_between", "Días entre dos fechas"), + ("weeks_between", "Semanas entre dos fechas"), + ("to_datetime", "Convertir a datetime.date"), + ("to_ordinal", "Ordinal (nº de días)"), + ] + + def today(cls) -> "Class_Date": + """Fecha actual (según *datetime.date.today()*).""" + return cls(datetime.date.today()) + + # Alias estático para consistencia con otras clases + Today = classmethod(today) + + +# ---------------------------------------------------------------------- +# Alias de usuario final — ajustado al sistema de tokenización existente +# ---------------------------------------------------------------------- + + +def Date(value: Union[str, datetime.date, int, sympy.Expr]): # noqa: N802 + """Alias que imita la sintaxis ``date('…')`` empleada por el usuario.""" + return Class_Date(value) + + +def Today(): # noqa: N802 + """Construye un objeto Date con la fecha actual (hoy).""" + return Class_Date.today() + + +# ---------------------------------------------------------------------- +# Registro para el sistema de tipos dinámico +# ---------------------------------------------------------------------- + + +def register_classes_in_module(): # pragma: no cover + """Devuelve la lista de clases para el registro automático.""" + return [ + ( + "Date", + Class_Date, + "SympyClassBase", + { + "add_lowercase": True, # permite usar ``date`` minúscula + "supports_brackets": False, + "description": "Fechas con aritmética y utilidades de calendario", + }, + ), + ( + "Today", + Today, + "function", + { + "add_lowercase": True, + "supports_brackets": False, + "description": "Fecha actual (hoy)", + }, + ), + ]