create_time_series.js

const sevenDays = 7 * 24 * 60 * 60 * 1000;
const daysOfWeek = [
  "Sunday",
  "Monday",
  "Tuesday",
  "Wednesday",
  "Thursday",
  "Friday",
  "Saturday",
  "Sunday",
];
const daysOfMonth = [
  "Jan",
  "Feb",
  "Mar",
  "Apr",
  "May",
  "Jun",
  "Jul",
  "Aug",
  "Sep",
  "Oct",
  "Nov",
  "Dec",
];

function advanceToDayOfWeek(date, dayOfWeek) {
  const pubDate = new Date(date.getTime());
  while (dayOfWeek && daysOfWeek[pubDate.getUTCDay()] !== dayOfWeek) {
    pubDate.setUTCDate(pubDate.getUTCDate() + 1);
  }
  return pubDate;
}

const periods = {
  weekly: (date) => date.setTime(date.getTime() + sevenDays),
  bimonthly: (date) => date.setUTCMonth(date.getUTCMonth() + 2),
  monthly: (date) =>
    date.getUTCDate() !== 1 ? date.setUTCDate(1) : date.setUTCMonth(date.getUTCMonth() + 1),
  biweekly: function (date) {
    const i = date.getUTCDate() === 1 ? 15 : 1;
    date.setUTCDate(i);
    if (i === 1) {
      date.setUTCMonth(date.getUTCMonth() + 1);
    }
  },
};

function formatDate(pubDate, format) {
  const monthYear = daysOfMonth[pubDate.getUTCMonth()] + " " + pubDate.getUTCFullYear();
  if (format === "full") {
    return daysOfWeek[pubDate.getUTCDay()] + " " + pubDate.getUTCDate() + " " + monthYear;
  }
  return monthYear;
}

// TODO: Seasonal, volume/issues?

/**
 * Generates a series of publication dates for a periodical, within a historical date range.
 * Useful for assigning realistic issue dates to magazines and newspapers.
 *
 * @param {Object} options - Time series options
 * @param {string} [options.period="weekly"] - Publication frequency: "weekly", "biweekly",
 *    "monthly", or "bimonthly".
 * @param {string} [options.dayOfWeek] - Advance each date to the next occurrence of this day
 *    (e.g. "Monday"). If not provided, dates are not adjusted to a specific weekday.
 * @param {string} [options.format="full"] - Date format: "full" produces "Monday 2 Jan 1956";
 *    any other value produces "Jan 1956".
 * @param {string|Date} [options.startDate="1956-01-01"] - Start of the date range, as a
 *    "YYYY-MM-DD" string or Date object.
 * @param {string|Date} [options.endDate="1958-07-14"] - End of the date range (exclusive), as a
 *    "YYYY-MM-DD" string or Date object.
 * @returns {string[]} An array of formatted date strings within the given range
 */
export function createTimeSeries({
  period = "weekly",
  dayOfWeek,
  format = "full",
  startDate = "1956-01-01",
  endDate = "1958-07-14",
}) {
  period = periods[period];
  if (!period) {
    throw new Error("Invalid period: " + period);
  }
  startDate = new Date(startDate);
  endDate = new Date(endDate);

  const dateStrings = [];
  for (let date = startDate; date < endDate; period(date)) {
    // pub date moves the date to a day of the week, but keeps calculating using the existing date.
    const pubDate = advanceToDayOfWeek(date, dayOfWeek);
    if (pubDate < endDate) {
      const dateString = formatDate(pubDate, format);
      dateStrings.push(dateString);
    }
  }
  return dateStrings;
}