text: // Usage: $ node public-holidays-ics-to-csv.js 2016,2017,2018
  // Since 2017, MOM's public ics has been changed to break up
  // holidays that span across multiple days into single-day events.
  // This scraper assumes that format and that all public holidays
  // are no longer than a single day.
  
  const fs = require('fs');
  const path = require('path');
  const axios = require('axios');
  const icalendar = require('icalendar');
  const { addDays, format, isSunday } = require('date-fns');
  
  const args = process.argv.slice(2);
  const years = args[0];
  if (!years) {
    console.error('Please specify the year(s).');
    process.exit(1);
  }
  
  const OUT_DIR = path.join(__dirname, 'holidays');
  if (!fs.existsSync(OUT_DIR)) {
    fs.mkdirSync(OUT_DIR);
  }
  
  async function fetchPublicHolidaysIcs(year) {
    console.log(`Fetching ICS for ${year}...`);
    const URL =
      'https://www.mom.gov.sg/~/media/mom/documents/employment-practices/' +
      `public-holidays/public-holidays-sg-${year}.ics`;
    const res = await axios.get(URL);
    console.log(`Fetched ICS for ${year}...`);
    return res.data;
  }
  
  years
    .split(',')
    .map((s) => s.trim())
    .forEach((year) => {
      fetchPublicHolidaysIcs(year)
        .then((icsData) => {
          // This trailing newline is needed by the parser else it's an invalid format.
          const ical = icalendar.parse_calendar(`${icsData}\n`);
          const calendarEvents = ical
            .events()
            .map((event) => ({
              date: new Date(event.getPropertyValue('DTSTART').valueOf()),
              name: event.getPropertyValue('SUMMARY'),
            }))
            // Order not guaranteed. Have to sort (in ascending order).
            .sort((a, b) => a.date - b.date);
  
          const data = [];
          const DATE_FORMAT = 'yyyy-MM-dd';
          for (let i = 0; i < calendarEvents.length; i++) {
            const event = calendarEvents[i];
            const { name, date } = event;
            let actualDay = true;
            // Mark Sunday holidays as observed next Monday.
            // Ignore Sundays that are the first day of CNY, as Monday will
            // already have a CNY Day 2 event.
            // FIXME: If first day of CNY is a Sunday, this code will not mark
            // Tuesday as a holiday.
            if (
              isSunday(date) &&
              i < calendarEvents.length &&
              calendarEvents[i + 1].name !== 'Chinese New Year'
            ) {
              actualDay = false;
            }
            data.push({
              date: format(date, DATE_FORMAT),
              name,
              day: format(date, 'EEEE'), // The day in text, e.g. Monday.
              observance: format(addDays(date, actualDay ? 0 : 1, 'day'), DATE_FORMAT),
              observance_strategy: actualDay ? 'actual_day' : 'next_monday',
            });
          }
  
          const HEADERS = ['Date', 'Name', 'Day', 'Observance', 'Observance Strategy'];
          const rows = [HEADERS];
          data.forEach((row) => {
            rows.push([row.date, row.name, row.day, row.observance, row.observance_strategy]);
          });
          const outPath = path.join(OUT_DIR, `${year}_singapore_holidays.csv`);
          console.log(`Writing holidays CSV for ${year} to ${outPath}`);
          fs.writeFileSync(outPath, `${rows.join('\n')}\n`);
        })
        .catch((err) => {
          console.error(err);
        });
    });
  