Generación de una cuadrícula de calendario mensual con soporte para calendario lunar en JavaScript

Al desarrollar interfaces de usuario que requieren la visualización de un calendario mensual, es común necesitar no solo las fechas del calendario gregoriano (solar), sino también las correspondientes al calendario lunar. A continuación, se detallan dos enfoques para generar la estructura de datos de un mes completo, incluyendo los días adyacentes del mes anterior y posterior para completar la cuadrícula, junto con la integración de un algoritmo de conversión lunar.

Primer enfoque: Cálculo manual de desplazamientos

Este método calcula explícitamente los días del mes anterior, actual y siguiente. Aunque es funcional, implica una mayor cantidad de operaciones y bucles anidados para construir una matriz bidimensional que luego se aplana. A continuación, se presenta una versión refactorizada utilizando JavaScript nativo para evitar dependencias de bibliotecas de fechas obsoletas.


const generateCalendarGrid = (targetDate, externalData = []) => {
  const year = targetDate.getFullYear();
  const month = targetDate.getMonth(); // Indexado desde 0

  const firstDayOfMonth = new Date(year, month, 1);
  const lastDayOfMonth = new Date(year, month + 1, 0);
  
  const startWeekday = firstDayOfMonth.getDay(); // 0 (Domingo) - 6 (Sábado)
  const daysInCurrentMonth = lastDayOfMonth.getDate();

  // Determinar si la cuadrícula necesita 5 o 6 filas (35 o 42 días)
  const totalCells = (daysInCurrentMonth + startWeekday > 35) ? 42 : 35;
  
  const gridData = [];
  const startDate = new Date(year, month, 1 - startWeekday);

  for (let i = 0; i < totalCells; i++) {
    const currentDate = new Date(startDate);
    currentDate.setDate(startDate.getDate() + i);
    
    const formattedDate = formatDate(currentDate);
    const [cYear, cMonth, cDay] = formattedDate.split('-').map(Number);
    
    // Conversión a calendario lunar
    const lunarInfo = LunarCalendarConverter.gregorianToLunar(cYear, cMonth, cDay);
    
    // Buscar datos externos asociados a esta fecha
    const matchedData = externalData.find(item => item.date === formattedDate);

    gridData.push({
      date: formattedDate,
      day: cDay,
      month: cMonth,
      lunarDay: lunarInfo ? lunarInfo.lunarDayStr : '',
      isCurrentMonth: cMonth === month + 1,
      extra: matchedData || null
    });
  }

  return gridData;
};

const formatDate = (date) => {
  const y = date.getFullYear();
  const m = String(date.getMonth() + 1).padStart(2, '0');
  const d = String(date.getDate()).padStart(2, '0');
  return `${y}-${m}-${d}`;
};

Segundo enfoque: Iteración secuencial optimizada

Para reducir la complejidad y mejorar la legibilidad, podemos calcular la fecha de inicio exacta y simplemente iterar día por día. Este enfoque es más directo y evita la manipulación compleja de índices, manteniendo el código limpio y fácil de mantener.


const buildOptimizedCalendar = (targetDate, events = []) => {
  const year = targetDate.getFullYear();
  const month = targetDate.getMonth();
  
  const firstDay = new Date(year, month, 1);
  const offset = firstDay.getDay();
  
  // Calcular el número total de días a mostrar (35 o 42)
  const daysInMonth = new Date(year, month + 1, 0).getDate();
  const totalDays = (daysInMonth + offset > 35) ? 42 : 35;
  
  const calendarDays = [];
  const iteratorDate = new Date(year, month, 1 - offset);

  for (let i = 0; i < totalDays; i++) {
    const current = new Date(iteratorDate);
    current.setDate(iteratorDate.getDate() + i);
    
    const isoDate = formatDate(current);
    const [y, m, d] = isoDate.split('-').map(Number);
    
    const lunarData = LunarCalendarConverter.gregorianToLunar(y, m, d);
    const event = events.find(e => e.date === isoDate);

    calendarDays.push({
      isoDate,
      day: d,
      month: m,
      lunar: lunarData ? lunarData.lunarDayStr : '',
      event: event ? event : undefined
    });
  }

  return calendarDays;
};

Módulo de conversión de calendario lunar

Para realizar la conversión entre el calendario gregoriano y el lunar, se utiliza una tabla de datos precalculada que cubre los años 1900 a 2100. A continuación, se presenta una versión refactorizada del módulo de conversión, utilizando clases de ES6 y nomenclatura más descriptiva para mejorar la encapsulación.


/**
 * Módulo de conversión de calendario solar a lunar (1900-2100)
 */

const LUNAR_DATA = [0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2, // 1900-1909
  0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977, // 1910-1919
  0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970, // 1920-1929
  0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950, // 1930-1939
  0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557, // 1940-1949
  0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0, // 1950-1959
  0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0, // 1960-1969
  0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6, // 1970-1979
  0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570, // 1980-1989
  0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x05ac0, 0x0ab60, 0x096d5, 0x092e0, // 1990-1999
  0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5, // 2000-2009
  0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930, // 2010-2019
  0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530, // 2020-2029
  0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45, // 2030-2039
  0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0, // 2040-2049
  0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06b20, 0x1a6c4, 0x0aae0, // 2050-2059
  0x0a2e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4, // 2060-2069
  0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0, // 2070-2079
  0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160, // 2080-2089
  0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252, // 2090-2099
  0x0d520]; // 2100

const ZODIAC_ANIMALS = ['Rata', 'Buey', 'Tigre', 'Conejo', 'Dragón', 'Serpiente', 'Caballo', 'Oveja', 'Mono', 'Gallo', 'Perro', 'Cerdo'];

class LunarCalendarConverter {
  static getLunarYearDays(year) {
    let sum = 348;
    for (let i = 0x8000; i > 0x8; i >>= 1) {
      sum += (LUNAR_DATA[year - 1900] & i) ? 1 : 0;
    }
    return sum + this.getLeapMonthDays(year);
  }

  static getLeapMonth(year) {
    return LUNAR_DATA[year - 1900] & 0xf;
  }

  static getLeapMonthDays(year) {
    if (this.getLeapMonth(year)) {
      return (LUNAR_DATA[year - 1900] & 0x10000) ? 30 : 29;
    }
    return 0;
  }

  static getRegularMonthDays(year, month) {
    if (month > 12 || month < 1) return -1;
    return (LUNAR_DATA[year - 1900] & (0x10000 >> month)) ? 30 : 29;
  }

  static gregorianToLunar(year, month, day) {
    if (year < 1900 || year > 2100) return null;
    if (year === 1900 && month === 1 && day < 31) return null;

    const baseDate = Date.UTC(1900, 0, 31);
    const targetDate = Date.UTC(year, month - 1, day);
    let offset = Math.floor((targetDate - baseDate) / 86400000);

    let lunarYear = 1900;
    let yearDays = 0;
    for (let i = 1900; i < 2101 && offset > 0; i++) {
      yearDays = this.getLunarYearDays(i);
      offset -= yearDays;
      lunarYear = i;
    }
    if (offset < 0) {
      offset += yearDays;
      lunarYear--;
    }

    let lunarMonth = 1;
    let isLeap = false;
    const leapMonth = this.getLeapMonth(lunarYear);

    for (let i = 1; i < 13 && offset > 0; i++) {
      if (leapMonth > 0 && i === (leapMonth + 1) && !isLeap) {
        --i;
        isLeap = true;
        yearDays = this.getLeapMonthDays(lunarYear);
      } else {
        yearDays = this.getRegularMonthDays(lunarYear, i);
      }

      if (isLeap && i === (leapMonth + 1)) {
        isLeap = false;
      }
      offset -= yearDays;
      lunarMonth = i;
    }

    if (offset === 0 && leapMonth > 0 && lunarMonth === leapMonth + 1) {
      if (isLeap) {
        isLeap = false;
      } else {
        isLeap = true;
        --lunarMonth;
      }
    }

    if (offset < 0) {
      offset += yearDays;
      --lunarMonth;
    }

    const lunarDay = offset + 1;

    return {
      lunarYear,
      lunarMonth,
      lunarDay,
      isLeapMonth: isLeap,
      zodiac: ZODIAC_ANIMALS[(lunarYear - 4) % 12],
      lunarDayStr: this.formatLunarDay(lunarDay),
      lunarMonthStr: this.formatLunarMonth(lunarMonth, isLeap)
    };
  }

  static formatLunarMonth(month, isLeap) {
    const monthNames = ['Zheng', 'Er', 'San', 'Si', 'Wu', 'Liu', 'Qi', 'Ba', 'Jiu', 'Shi', 'Dong', 'La'];
    return (isLeap ? 'Bisiesto ' : '') + monthNames[month - 1];
  }

  static formatLunarDay(day) {
    const tens = ['Inicial', 'Diez', 'Veinte', 'Treinta'];
    const digits = ['', 'Uno', 'Dos', 'Tres', 'Cuatro', 'Cinco', 'Seis', 'Siete', 'Ocho', 'Nueve', 'Diez'];
    
    if (day === 10) return 'Diez';
    if (day === 20) return 'Veinte';
    if (day === 30) return 'Treinta';
    
    return tens[Math.floor(day / 10)] + ' ' + digits[day % 10];
  }
}

export default LunarCalendarConverter;

Etiquetas: JavaScript calendario-lunar manipulacion-de-fechas ES6 frontend

Publicado el 7-19 09:17