Source: index.js

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.RelativeDateEntry = exports.ExactDateWithYearEntry = exports.ExactDateEntry = exports.Entry = void 0;
exports.getAllEntries = getAllEntries;
exports.getEntries = getEntries;
exports.getEntriesByType = getEntriesByType;
exports.getEntriesInNextDays = getEntriesInNextDays;
exports.getEntriesInNextMonths = getEntriesInNextMonths;
exports.getEntriesInNextWeeks = getEntriesInNextWeeks;
exports.getEntriesInNextYears = getEntriesInNextYears;
exports.getEntriesOnDate = getEntriesOnDate;
exports.getEntriesOnMonthDay = getEntriesOnMonthDay;
exports.parseCSVLine = parseCSVLine;
var _fs = _interopRequireDefault(require("fs"));
var _path = _interopRequireDefault(require("path"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
/**
 * Represents a calendar entry with a name and date.
 * Base class for all entry types.
 */
class Entry {
  constructor(name, source) {
    this.name = name;
    this.source = source;
  }

  /**
   * Gets the next occurrence of this entry from the given date.
   * @param fromDate - The date to calculate from (default: today)
   * @returns The next occurrence as a Date object
   */

  /**
   * Checks if this entry occurs on the given date.
   * @param date - The date to check
   * @returns true if the entry occurs on this date
   */

  /**
   * Gets all occurrences within a date range.
   * @param startDate - Start of the range
   * @param endDate - End of the range
   * @returns Array of dates when this entry occurs
   */
}

/**
 * Entry with a specific month and day (recurring annually).
 * Example: "New Year's Day,01/01"
 */
exports.Entry = Entry;
class ExactDateEntry extends Entry {
  constructor(name, month, day, source) {
    super(name, source);
    this.month = month;
    this.day = day;
    this.source = source;
  }
  getNextOccurrence(fromDate = new Date()) {
    const year = fromDate.getFullYear();
    let nextDate = new Date(year, this.month - 1, this.day);
    if (nextDate <= fromDate) {
      nextDate = new Date(year + 1, this.month - 1, this.day);
    }
    return nextDate;
  }
  occursOn(date) {
    return date.getMonth() === this.month - 1 && date.getDate() === this.day;
  }
  getOccurrencesInRange(startDate, endDate) {
    const occurrences = [];
    const startYear = startDate.getFullYear();
    const endYear = endDate.getFullYear();
    for (let year = startYear; year <= endYear; year++) {
      const occurrence = new Date(year, this.month - 1, this.day);
      if (occurrence >= startDate && occurrence <= endDate) {
        occurrences.push(occurrence);
      }
    }
    return occurrences;
  }
}

/**
 * Entry with a specific year, month, and day (one-time event).
 * Example: "Afghanistan,08/19,1919"
 */
exports.ExactDateEntry = ExactDateEntry;
class ExactDateWithYearEntry extends Entry {
  constructor(name, month, day, year, source) {
    super(name, source);
    this.month = month;
    this.day = day;
    this.year = year;
    this.source = source;
  }
  getNextOccurrence(fromDate = new Date()) {
    // For historical events, return the next anniversary
    const currentYear = fromDate.getFullYear();
    let anniversaryDate = new Date(currentYear, this.month - 1, this.day);
    if (anniversaryDate <= fromDate) {
      anniversaryDate = new Date(currentYear + 1, this.month - 1, this.day);
    }
    return anniversaryDate;
  }
  occursOn(date) {
    return date.getFullYear() === this.year && date.getMonth() === this.month - 1 && date.getDate() === this.day;
  }
  getOccurrencesInRange(startDate, endDate) {
    const eventDate = new Date(this.year, this.month - 1, this.day);
    if (eventDate >= startDate && eventDate <= endDate) {
      return [eventDate];
    }
    return [];
  }

  /**
   * Gets the anniversary of this event on a specific year.
   * @param year - The year to calculate the anniversary for
   * @returns The anniversary date
   */
  getAnniversary(year) {
    return new Date(year, this.month - 1, this.day);
  }

  /**
   * Gets the number of years since this event for a given date.
   * @param date - The date to calculate from (default: today)
   * @returns The number of years
   */
  getYearsSince(date = new Date()) {
    return date.getFullYear() - this.year;
  }
}

/**
 * Entry for a relative date (e.g., "3rd Monday in January").
 * Example: "Martin Luther King Jr. Day,3MondayJan"
 */
exports.ExactDateWithYearEntry = ExactDateWithYearEntry;
class RelativeDateEntry extends Entry {
  static DAYS_OF_WEEK = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
  static MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
  constructor(name, occurrence,
  // 1-5 (1st, 2nd, 3rd, 4th, 5th)
  dayOfWeek,
  // 0-6 (Sunday-Saturday)
  month,
  // 1-12
  source) {
    super(name, source);
    this.occurrence = occurrence;
    this.dayOfWeek = dayOfWeek;
    this.month = month;
    this.source = source;
  }

  /**
   * Parses a relative date string (e.g., "3MondayJan") into components.
   * @param dateStr - The date string to parse
   * @returns Object with occurrence, dayOfWeek, and month
   */
  static parseRelativeDate(dateStr) {
    const occurrenceMatch = dateStr.match(/^(\d)/);
    if (!occurrenceMatch || !occurrenceMatch[1]) throw new Error('Invalid relative date format');
    const occurrence = parseInt(occurrenceMatch[1], 10);
    let dayOfWeek = -1;
    for (let i = 0; i < this.DAYS_OF_WEEK.length; i++) {
      const dayName = this.DAYS_OF_WEEK[i];
      if (dayName && dateStr.includes(dayName)) {
        dayOfWeek = i;
        break;
      }
    }
    if (dayOfWeek === -1) throw new Error('Invalid day of week');
    let month = -1;
    for (let i = 0; i < this.MONTHS.length; i++) {
      const monthName = this.MONTHS[i];
      if (monthName && dateStr.endsWith(monthName)) {
        month = i + 1;
        break;
      }
    }
    if (month === -1) throw new Error('Invalid month');
    return {
      occurrence,
      dayOfWeek,
      month
    };
  }

  /**
   * Calculates the Nth occurrence of a weekday in a given month/year.
   * @param year - The year
   * @param month - The month (1-12)
   * @param dayOfWeek - Day of week (0-6, Sunday-Saturday)
   * @param occurrence - Which occurrence (1-5)
   * @returns The calculated date or null if it doesn't exist
   */
  static getNthWeekdayOfMonth(year, month, dayOfWeek, occurrence) {
    const firstDay = new Date(year, month - 1, 1);
    const firstDayOfWeek = firstDay.getDay();
    let daysToAdd = (dayOfWeek - firstDayOfWeek + 7) % 7;
    daysToAdd += (occurrence - 1) * 7;
    const targetDate = new Date(year, month - 1, 1 + daysToAdd);

    // Check if the date is still in the same month
    if (targetDate.getMonth() !== month - 1) {
      return null;
    }
    return targetDate;
  }
  getNextOccurrence(fromDate = new Date()) {
    let year = fromDate.getFullYear();
    let date = RelativeDateEntry.getNthWeekdayOfMonth(year, this.month, this.dayOfWeek, this.occurrence);
    if (!date || date <= fromDate) {
      year++;
      date = RelativeDateEntry.getNthWeekdayOfMonth(year, this.month, this.dayOfWeek, this.occurrence);
    }
    return date || new Date(9999, 11, 31);
  }
  occursOn(date) {
    if (date.getMonth() !== this.month - 1) return false;
    if (date.getDay() !== this.dayOfWeek) return false;
    const firstDay = new Date(date.getFullYear(), this.month - 1, 1);
    const firstDayOfWeek = firstDay.getDay();
    const daysToAdd = (this.dayOfWeek - firstDayOfWeek + 7) % 7;
    const expectedDay = 1 + daysToAdd + (this.occurrence - 1) * 7;
    return date.getDate() === expectedDay;
  }
  getOccurrencesInRange(startDate, endDate) {
    const occurrences = [];
    const startYear = startDate.getFullYear();
    const endYear = endDate.getFullYear();
    for (let year = startYear; year <= endYear; year++) {
      const occurrence = RelativeDateEntry.getNthWeekdayOfMonth(year, this.month, this.dayOfWeek, this.occurrence);
      if (occurrence && occurrence >= startDate && occurrence <= endDate) {
        occurrences.push(occurrence);
      }
    }
    return occurrences;
  }
}

/**
 * Parses a CSV file and returns an array of Entry objects.
 * Automatically detects the format of each line.
 * @param filePath - The path to the CSV file
 * @returns Array of Entry objects
 */
exports.RelativeDateEntry = RelativeDateEntry;
function getEntries(filePath, dataDir = './src/data') {
  const relative = _path.default.relative(dataDir, filePath).split(_path.default.sep).join('/');
  const data = _fs.default.readFileSync(filePath, 'utf-8');
  const lines = data.split('\n').filter(line => line.trim() !== '');
  const entries = [];
  for (const line of lines) {
    const parsed = parseCSVLine(line, relative);
    if (parsed) {
      entries.push(parsed);
    }
  }
  return entries;
}

/**
 * Parses a single CSV line into an Entry object.
 * Supports three formats:
 * 1. "Name,MM/DD" - Exact date (recurring)
 * 2. "Name,MM/DD,YYYY" - Exact date with year (one-time)
 * 3. "Name,NWeekdayMonth" - Relative date (e.g., "3MondayJan")
 * @param line - The CSV line to parse
 * @returns An Entry object or null if invalid
 */
function parseCSVLine(line, source) {
  const parts = line.split(',');
  if (parts.length < 2) return null;
  const name = parts[0]?.trim();
  const dateStr = parts[1]?.trim();
  if (!name || !dateStr) return null;

  // Format 1 & 2: MM/DD or MM/DD,YYYY
  if (dateStr.includes('/')) {
    const [monthStr, dayStr] = dateStr.split('/');
    if (!monthStr || !dayStr) return null;
    const month = parseInt(monthStr, 10);
    const day = parseInt(dayStr, 10);
    if (isNaN(month) || isNaN(day)) return null;

    // Check if there's a year
    if (parts.length >= 3) {
      const yearStr = parts[2]?.trim();
      if (yearStr) {
        const year = parseInt(yearStr, 10);
        if (!isNaN(year)) {
          return new ExactDateWithYearEntry(name, month, day, year, source);
        }
      }
    }
    return new ExactDateEntry(name, month, day, source);
  }

  // Format 3: NWeekdayMonth (e.g., "3MondayJan")
  try {
    const {
      occurrence,
      dayOfWeek,
      month
    } = RelativeDateEntry.parseRelativeDate(dateStr);
    return new RelativeDateEntry(name, occurrence, dayOfWeek, month, source);
  } catch (error) {
    return null;
  }
}

/**
 * Reads all .csv files in the data directory and its subdirectories.
 * @param dataDir - The root data directory (default: './src/data')
 * @returns Array of all Entry objects
 */
function getAllEntries(dataDir = './src/data') {
  const allEntries = [];
  function readDirRecursively(dirPath) {
    const items = _fs.default.readdirSync(dirPath, {
      withFileTypes: true
    });
    for (const item of items) {
      const fullPath = _path.default.join(dirPath, item.name);
      if (item.isDirectory()) {
        readDirRecursively(fullPath);
      } else if (item.isFile() && item.name.endsWith('.csv')) {
        const entries = getEntries(fullPath, dataDir);
        allEntries.push(...entries);
      }
    }
  }
  readDirRecursively(dataDir);
  return allEntries;
}

/**
 * Gets entries that occur within the next N days.
 * @param entries - Array of entries to filter
 * @param days - Number of days to look ahead
 * @param fromDate - Starting date (default: today)
 * @returns Array of entries with their next occurrence dates
 */
function getEntriesInNextDays(entries, days, fromDate = new Date()) {
  const endDate = new Date(fromDate);
  endDate.setDate(endDate.getDate() + days);
  return entries.map(entry => ({
    entry,
    date: entry.getNextOccurrence(fromDate)
  })).filter(({
    date
  }) => date >= fromDate && date <= endDate).sort((a, b) => a.date.getTime() - b.date.getTime());
}

/**
 * Gets entries that occur within the next N weeks.
 * @param entries - Array of entries to filter
 * @param weeks - Number of weeks to look ahead
 * @param fromDate - Starting date (default: today)
 * @returns Array of entries with their next occurrence dates
 */
function getEntriesInNextWeeks(entries, weeks, fromDate = new Date()) {
  return getEntriesInNextDays(entries, weeks * 7, fromDate);
}

/**
 * Gets entries that occur within the next N months.
 * @param entries - Array of entries to filter
 * @param months - Number of months to look ahead
 * @param fromDate - Starting date (default: today)
 * @returns Array of entries with their next occurrence dates
 */
function getEntriesInNextMonths(entries, months, fromDate = new Date()) {
  const endDate = new Date(fromDate);
  endDate.setMonth(endDate.getMonth() + months);
  return entries.map(entry => ({
    entry,
    date: entry.getNextOccurrence(fromDate)
  })).filter(({
    date
  }) => date >= fromDate && date <= endDate).sort((a, b) => a.date.getTime() - b.date.getTime());
}

/**
 * Gets entries that occur within the next N years.
 * @param entries - Array of entries to filter
 * @param years - Number of years to look ahead
 * @param fromDate - Starting date (default: today)
 * @returns Array of entries with their next occurrence dates
 */
function getEntriesInNextYears(entries, years, fromDate = new Date()) {
  const endDate = new Date(fromDate);
  endDate.setFullYear(endDate.getFullYear() + years);
  return entries.map(entry => ({
    entry,
    date: entry.getNextOccurrence(fromDate)
  })).filter(({
    date
  }) => date >= fromDate && date <= endDate).sort((a, b) => a.date.getTime() - b.date.getTime());
}

/**
 * Gets entries that occur on a specific date.
 * @param entries - Array of entries to filter
 * @param date - The date to check
 * @returns Array of entries that occur on this date
 */
function getEntriesOnDate(entries, date) {
  return entries.filter(entry => entry.occursOn(date));
}

/**
 * Gets entries that occur on a specific month and day (any year).
 * @param entries - Array of entries to filter
 * @param month - Month (1-12)
 * @param day - Day of month
 * @returns Array of entries that occur on this month/day
 */
function getEntriesOnMonthDay(entries, month, day) {
  return entries.filter(entry => {
    if (entry instanceof ExactDateEntry) {
      return entry.month === month && entry.day === day;
    }
    if (entry instanceof ExactDateWithYearEntry) {
      return entry.month === month && entry.day === day;
    }
    return false;
  });
}

/**
 * Gets entries by type.
 * @param entries - Array of entries to filter
 * @param type - The entry type to filter by
 * @returns Array of entries of the specified type
 */
function getEntriesByType(entries, type) {
  return entries.filter(entry => entry instanceof type);
}