Files
math2-platform/backend/docs/streak-calculator.md
Renato bc43c9e772
Some checks failed
Test Suite / test-backend (push) Has been cancelled
Test Suite / test-frontend (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / coverage-check (push) Has been cancelled
🎓 Initial commit: Math2 Platform - Plataforma de Álgebra Lineal PRO
 Características:
- 45 ejercicios universitarios (Basic → Advanced)
- Renderizado LaTeX profesional
- IA generativa (Z.ai/DashScope)
- Docker 9 servicios
- Tests 123/123 pasando
- Seguridad enterprise (JWT, XSS, Rate limiting)

🐳 Infraestructura:
- Next.js 14 + Node.js 20
- PostgreSQL 15 + Redis 7
- Docker Compose completo
- Nginx + SSL ready

📚 Documentación:
- 5 informes técnicos completos
- README profesional
- Scripts de deployment automatizados

Estado: Producción lista 
2026-03-31 11:27:11 -03:00

6.0 KiB

Streak Calculator - Documentación Técnica

Resumen del Fix

Problema Identificado (Issue #10)

El cálculo anterior de streak en score.calculator.ts (líneas 187-193) tenía una inconsistencia grave:

// PROBLEMA: Cálculo inconsistente de daysDiff
const latestAttempt = new Date(attempts[0].createdAt);
latestAttempt.setHours(0, 0, 0, 0);
const daysDiff = Math.floor((today.getTime() - latestAttempt.getTime()) / (1000 * 60 * 60 * 24));

// Caso problemático:
// - Hoy es lunes 9:00 AM
// - Último attempt fue lunes 2:00 AM  
// - daysDiff = 0 (mismo día calendario)
// - PERO: El usuario NO completó ejercicios "ayer" (domingo)
// - Resultado: Streak incorrectamente activo

Solución Implementada

Se creó StreakCalculator con manejo robusto de timezones usando date-fns y date-fns-tz.

Arquitectura

Componentes Principales

  1. StreakCalculator (streak.calculator.ts)

    • Cálculo timezone-aware
    • Algoritmo de ventana deslizante para longest streak
    • Manejo de DST (Daylight Saving Time)
    • Caché implícita de 1 minuto (cálculos costosos)
  2. ScoreCalculator (actualizado)

    • Delega cálculo de streak a StreakCalculator
    • Mantiene interfaz existente para compatibilidad
  3. Endpoint /api/ranking/streak

    • Devuelve streak completo con metadata
    • Requiere autenticación
    • Usa timezone del usuario desde base de datos

Algoritmo de Cálculo

Fase 1: Normalización de Timezone

// Obtener "hoy" en el timezone del usuario
const now = new Date();
const today = startOfDay(toZonedTime(now, timezone));

Fase 2: Obtención de Datos

  • Query a Prisma: últimos 3 días de actividad
  • Conversión de fechas UTC a timezone local
  • Eliminación de duplicados (múltiples ejercicios mismo día)

Fase 3: Verificación de Streak Activo

isStreakActive(lastActivity: Date, today: Date): boolean {
  const diff = differenceInCalendarDays(today, lastActivity);
  return diff <= 1; // Hoy (0) o ayer (1)
}

Fase 4: Cálculo de Días Consecutivos

calculateConsecutiveDays(sortedDays: Date[]): number {
  let streak = 1;
  for (let i = 0; i < sortedDays.length - 1; i++) {
    const diff = differenceInCalendarDays(sortedDays[i], sortedDays[i+1]);
    if (diff === 1) streak++;
    else if (diff > 1) break;
  }
  return streak;
}

Fase 5: Longest Streak Histórico

Algoritmo de ventana deslizante O(n):

let maxStreak = 1, currentStreak = 1;
for (let i = 1; i < sortedDays.length; i++) {
  const diff = differenceInCalendarDays(sortedDays[i], sortedDays[i-1]);
  if (diff === 1) {
    currentStreak++;
    maxStreak = Math.max(maxStreak, currentStreak);
  } else if (diff > 1) {
    currentStreak = 1;
  }
}

Manejo de Edge Cases

1. Timezone Boundaries

Escenario: Usuario en Argentina (UTC-3) completa ejercicio a las 23:00 hora local.

  • UTC: 02:00 del día siguiente
  • Local: 23:00 del día actual

Solución: toZonedTime() convierte UTC a hora local antes de comparar días.

2. Daylight Saving Time (DST)

Escenario: Cambio de horario de verano en NY (1 hora "perdida" o "ganada")

Solución: date-fns-tz maneja automáticamente DST, calculando días calendario reales.

3. Viaje entre Timezones

Escenario: Usuario viaja de NY → Tokyo

Comportamiento:

  • Streak se mantiene activo/inactivo independientemente del timezone
  • currentStreak puede variar según la hora local
  • Se usa el timezone de preferencia del usuario (almacenado en DB)

4. Múltiples Ejercicios mismo día

Solución: Set para eliminar duplicados antes de contar días.

API Response

GET /api/ranking/streak

{
  "success": true,
  "data": {
    "currentStreak": 5,
    "longestStreak": 12,
    "lastActivityDate": "2024-03-30T00:00:00.000Z",
    "isStreakActive": true,
    "daysUntilStreakBreaks": 1
  },
  "meta": {
    "timestamp": "2024-03-30T14:30:00.000Z",
    "timezone": "America/Argentina/Buenos_Aires"
  }
}

Interpretación de daysUntilStreakBreaks

  • 1.0: Actividad hoy → tiene hasta mañana (24+ horas)
  • 0.x: Actividad ayer → debe actuar hoy (menos de 24 horas restantes)
  • 0: Streak inactivo o roto

Migración de Datos

Nuevo Campo en User

model User {
  // ... campos existentes
  timezone String @default("UTC")
}

Script de Migración Sugerido

// Para usuarios existentes, asignar timezone basado en preferencias o default UTC
await prisma.user.updateMany({
  where: { timezone: null },
  data: { timezone: 'UTC' }
});

Tests

Cobertura

  1. Streak básico (0, 1, N días)
  2. Rotura de streak (2+ días sin actividad)
  3. Timezone: Argentina (UTC-3)
  4. Timezone: Viaje NY → Tokyo
  5. DST: New York
  6. DST: Europa
  7. Múltiples ejercicios mismo día
  8. Longest streak histórico
  9. Activity check por fecha
  10. Days until break calculation

Comando de Ejecución

cd /home/ren/Documents/math2/backend
npm test -- streak.calculator.test.ts

Dependencias

{
  "dependencies": {
    "date-fns": "^3.x",
    "date-fns-tz": "^3.x"
  }
}

Instalación:

npm install date-fns date-fns-tz

Optimizaciones Futuras

  1. Redis Cache: Cachear streak por 1 minuto para reducir queries
  2. Batch Processing: Calcular streaks en background cada hora
  3. WebSocket: Notificar cuando queden pocas horas para mantener streak
  4. Push Notifications: Recordatorio diario configurable

Changelog

v2.0.0 (2024-03-30)

  • Fix: Inconsistencia en cálculo de streak (Issue #10)
  • Feature: Soporte de timezones con date-fns-tz
  • Feature: Endpoint GET /api/ranking/streak
  • Feature: Campo timezone en modelo User
  • Feature: Algoritmo de longest streak
  • Tests: 20+ casos de prueba incluyendo DST y timezones

Referencias