Docs/Skills and code/src/

src/analysis/temporal.js

Code121 lines5.7 KBsrc/

const DAY_MS = 86400000;
const PRECISE_FOR_RECENCY = new Set([
  "exact_datetime", "exact_day", "estimated_minute", "estimated_hour",
  "estimated_day", "estimated_week"
]);
const MONTHS = new Map([
  ["jan", 0], ["january", 0], ["feb", 1], ["february", 1], ["mar", 2], ["march", 2],
  ["apr", 3], ["april", 3], ["may", 4], ["jun", 5], ["june", 5], ["jul", 6], ["july", 6],
  ["aug", 7], ["august", 7], ["sep", 8], ["sept", 8], ["september", 8], ["oct", 9],
  ["october", 9], ["nov", 10], ["november", 10], ["dec", 11], ["december", 11]
]);

function validDate(value) {
  if (!value) return null;
  const date = new Date(value);
  return Number.isFinite(date.getTime()) ? date : null;
}

function validUtcDay(year, month, day) {
  const timestamp = Date.UTC(year, month, day);
  const date = new Date(timestamp);
  if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month || date.getUTCDate() !== day) return null;
  return date;
}

function stripDisplayPrefix(value) {
  return String(value ?? "").trim().replace(/^(?:(?:first\s+)?streamed|started\s+streaming|premiered|published|uploaded|posted|originally\s+aired|aired)(?:\s+on)?\s+/i, "").trim();
}

function parseAbsoluteDisplay(value) {
  const text = stripDisplayPrefix(value).replace(/^on\s+/i, "").trim();
  const isoDay = text.match(/^(\d{4})-(\d{2})-(\d{2})$/);
  if (isoDay) return validUtcDay(Number(isoDay[1]), Number(isoDay[2]) - 1, Number(isoDay[3]));

  // Common YouTube display dates: "Sep 19, 2026", "September 19, 2026", "19 Sep 2026".
  let match = text.match(/^([A-Za-z]+)\s+(\d{1,2}),?\s+(\d{4})$/);
  if (match && MONTHS.has(match[1].toLowerCase())) {
    return validUtcDay(Number(match[3]), MONTHS.get(match[1].toLowerCase()), Number(match[2]));
  }
  match = text.match(/^(\d{1,2})\s+([A-Za-z]+)\s+(\d{4})$/);
  if (match && MONTHS.has(match[2].toLowerCase())) {
    return validUtcDay(Number(match[3]), MONTHS.get(match[2].toLowerCase()), Number(match[1]));
  }

  // ISO timestamps and other explicit timestamps are accepted only when they include a time/zone.
  if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(text)) return validDate(text);
  return null;
}

function subtractCalendar(anchor, amount, unit) {
  const date = new Date(anchor.getTime());
  if (unit === "month" || unit === "year") {
    const originalDay = date.getUTCDate();
    date.setUTCDate(1);
    if (unit === "year") date.setUTCFullYear(date.getUTCFullYear() - amount);
    else date.setUTCMonth(date.getUTCMonth() - amount);
    const lastDay = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0)).getUTCDate();
    date.setUTCDate(Math.min(originalDay, lastDay));
    return date;
  }
  date.setTime(date.getTime() - amount * ({ minute: 60000, hour: 3600000, day: DAY_MS, week: 7 * DAY_MS }[unit] ?? DAY_MS));
  return date;
}

/** Parse dates conservatively. Relative dates are estimates anchored to when the row was discovered. */
export function normalizePublishedAt(rawValue, discoveredAt) {
  const raw = String(rawValue ?? "").trim();
  if (!raw) return { normalized: null, precision: "unknown", method: "empty_source" };

  const absolute = parseAbsoluteDisplay(raw);
  if (absolute) {
    const originalHasTime = /(?:T\d{2}:\d{2}|\s\d{1,2}:\d{2}(?::\d{2})?)/i.test(stripDisplayPrefix(raw));
    const hadPrefix = stripDisplayPrefix(raw) !== raw;
    return {
      normalized: absolute.toISOString(),
      precision: originalHasTime ? "exact_datetime" : "exact_day",
      method: hadPrefix ? "absolute_display_value" : "absolute_source_value"
    };
  }

  const anchor = validDate(discoveredAt);
  if (!anchor) return { normalized: null, precision: "unknown", method: "relative_without_anchor" };
  const lower = stripDisplayPrefix(raw).toLowerCase().replace(/\s+/g, " ");
  if (/^(just now|live now)$/.test(lower)) {
    return { normalized: anchor.toISOString(), precision: "estimated_minute", method: "relative_to_discovered_at" };
  }
  if (lower === "yesterday") {
    const date = subtractCalendar(anchor, 1, "day");
    return { normalized: date.toISOString(), precision: "estimated_day", method: "relative_to_discovered_at" };
  }
  if (lower === "today") return { normalized: anchor.toISOString(), precision: "estimated_day", method: "relative_to_discovered_at" };

  const match = lower.match(/(?:published\s+)?(\d+(?:\.\d+)?)\s*(minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|wks?|w|months?|mos?|mo|years?|yrs?|yr|y)\s+ago/);
  if (!match) return { normalized: null, precision: "unknown", method: "unrecognized_source_value" };

  const amount = Number(match[1]);
  const unitText = match[2];
  let unit;
  let precision;
  if (/^(m|min|mins|minute|minutes)$/.test(unitText)) { unit = "minute"; precision = "estimated_minute"; }
  else if (/^(h|hr|hrs|hour|hours)$/.test(unitText)) { unit = "hour"; precision = "estimated_hour"; }
  else if (/^(d|day|days)$/.test(unitText)) { unit = "day"; precision = "estimated_day"; }
  else if (/^(w|wk|wks|week|weeks)$/.test(unitText)) { unit = "week"; precision = "estimated_week"; }
  else if (/^(mo|mos|month|months)$/.test(unitText)) { unit = "month"; precision = "estimated_month"; }
  else { unit = "year"; precision = "estimated_year"; }

  if (!Number.isFinite(amount) || amount < 0) return { normalized: null, precision: "unknown", method: "invalid_relative_amount" };
  return { normalized: subtractCalendar(anchor, amount, unit).toISOString(), precision, method: "relative_to_discovered_at" };
}

export function isRecencyEligible(precision) {
  return PRECISE_FOR_RECENCY.has(precision);
}

export function ageInDays(normalizedValue, now = new Date()) {
  const date = validDate(normalizedValue);
  if (!date) return null;
  const age = (now.getTime() - date.getTime()) / DAY_MS;
  return Number.isFinite(age) && age >= 0 ? Math.max(1, age) : null;
}

Shown as it is in the repository at build time (2026-11-03); the home directory appears as <home>. If this page and the file differ, the file is right. Back to the catalogue.