Agregado de Date
This commit is contained in:
parent
6ebc81d0b9
commit
a85434d492
|
@ -1,5 +1,41 @@
|
||||||
valor = 500
|
# Italiano
|
||||||
k = valor * symbols('Hz') / symbols('mmS')
|
faltan_italiano = 51
|
||||||
|
hechas_italiano = 21
|
||||||
|
total_italiano = faltan_italiano+hechas_italiano
|
||||||
|
|
||||||
valor_mmS = 100 * symbols('mmS')
|
# Matematica
|
||||||
speed = k * valor_mmS
|
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=?
|
||||||
|
|
|
@ -407,6 +407,13 @@ class PureAlgebraicEngine:
|
||||||
eval_context = self._get_complete_context()
|
eval_context = self._get_complete_context()
|
||||||
result_obj = sympify(expression_str, locals=eval_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)
|
# 1. ASIGNACIÓN AL CONTEXTO (para evaluación directa)
|
||||||
self.symbol_table[var_name] = result_obj
|
self.symbol_table[var_name] = result_obj
|
||||||
self.variables.add(var_name)
|
self.variables.add(var_name)
|
||||||
|
@ -509,6 +516,13 @@ class PureAlgebraicEngine:
|
||||||
eval_context = self._get_complete_context()
|
eval_context = self._get_complete_context()
|
||||||
result_obj = sympify(line, locals=eval_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
|
# Si es un PlotResult, asegurar que tenga la línea original
|
||||||
if isinstance(result_obj, PlotResult) and not result_obj.original_expression:
|
if isinstance(result_obj, PlotResult) and not result_obj.original_expression:
|
||||||
result_obj.original_expression = line
|
result_obj.original_expression = line
|
||||||
|
@ -600,15 +614,33 @@ class PureAlgebraicEngine:
|
||||||
|
|
||||||
# Verificar que el resultado no sea problemático
|
# 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)):
|
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)
|
return Eq(var_symbol, final_value)
|
||||||
else:
|
else:
|
||||||
# Si hay problema con la resolución, devolver simbólico
|
# 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)
|
return Eq(var_symbol, solution_value)
|
||||||
else:
|
else:
|
||||||
# Hay variables con valores simbólicos, devolver ecuación simbólica
|
# Hay variables con valores simbólicos, devolver ecuación simbólica
|
||||||
return Eq(var_symbol, solution_value)
|
return Eq(var_symbol, solution_value)
|
||||||
else:
|
else:
|
||||||
# No hay variables sin resolver, es ya un valor final
|
# 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)
|
return Eq(var_symbol, solution_value)
|
||||||
else:
|
else:
|
||||||
# Si no hay solución en las ecuaciones, verificar en symbol_table
|
# Si no hay solución en las ecuaciones, verificar en symbol_table
|
||||||
|
@ -617,6 +649,12 @@ class PureAlgebraicEngine:
|
||||||
value = self.symbol_table[var_name]
|
value = self.symbol_table[var_name]
|
||||||
# Si el valor en symbol_table es diferente de la variable, devolverlo
|
# Si el valor en symbol_table es diferente de la variable, devolverlo
|
||||||
if value != var_symbol:
|
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)
|
return Eq(var_symbol, value)
|
||||||
|
|
||||||
# Si no hay información, devolver ecuación con la variable sin resolver
|
# 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}")
|
self.logger.debug(f"Error resolviendo {var_symbol}: {e}")
|
||||||
return var_symbol
|
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):
|
def _resolve_iteratively(self, expression, max_iterations=10):
|
||||||
"""Resuelve una expresión iterativamente sustituyendo valores conocidos"""
|
"""Resuelve una expresión iterativamente sustituyendo valores conocidos"""
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -315,10 +315,21 @@ class HybridCalculatorPySide6(QMainWindow):
|
||||||
self.latex_button.setChecked(True)
|
self.latex_button.setChecked(True)
|
||||||
self.latex_panel_visible = True
|
self.latex_panel_visible = True
|
||||||
|
|
||||||
# Configurar tamaños
|
# Configurar tamaños con límites
|
||||||
latex_width = self.settings.get("latex_panel_width", 300)
|
latex_width = self.settings.get("latex_panel_width", 300)
|
||||||
total_width = self.width()
|
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
|
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])
|
self.secondary_splitter.setSizes([main_width, latex_width])
|
||||||
|
|
||||||
# Actualizar panel con ecuaciones pendientes
|
# Actualizar panel con ecuaciones pendientes
|
||||||
|
@ -346,11 +357,17 @@ class HybridCalculatorPySide6(QMainWindow):
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
"""Maneja el cierre de la aplicación"""
|
"""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:
|
if self.latex_panel_visible and self.secondary_splitter.count() > 1:
|
||||||
sizes = self.secondary_splitter.sizes()
|
sizes = self.secondary_splitter.sizes()
|
||||||
if len(sizes) > 1:
|
if len(sizes) > 1:
|
||||||
latex_width = 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)
|
self.settings_manager.set_setting("latex_panel_width", latex_width)
|
||||||
|
|
||||||
# Delegar al settings manager
|
# Delegar al settings manager
|
||||||
|
|
|
@ -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)",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
Loading…
Reference in New Issue