code icon Code

Time Distribution Analysis

Analyze hourly and daily distribution of timestamps

Source Code

import fs from "fs";
import path from "path";

const [inputPath, dateField, outputPath] = process.argv.slice(2);

if (!inputPath || !dateField || !outputPath) {
  console.error("Usage: stdlib.time.distribute <inputPath> <dateField> <outputPath>");
  process.exit(1);
}

const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
const HOUR_LABELS = Array.from({ length: 24 }, (_, i) => `${String(i).padStart(2, "0")}:00`);

/**
 * Get nested field value
 */
function getField(obj, fieldPath) {
  const parts = fieldPath.split(".");
  let value = obj;
  for (const part of parts) {
    if (value == null) return undefined;
    value = value[part];
  }
  return value;
}

try {
  console.log(`Reading ${inputPath}...`);
  const raw = fs.readFileSync(inputPath, "utf-8");
  const data = JSON.parse(raw);

  const items = Array.isArray(data)
    ? data
    : data.items || data.results || data.messages || [];

  if (!Array.isArray(items)) {
    console.error("Input must be a JSON array or object with array property");
    process.exit(1);
  }

  console.log(`Analyzing ${items.length} timestamps...`);

  // Initialize distributions
  const hourDistribution = new Array(24).fill(0);
  const dayDistribution = new Array(7).fill(0);
  const hourlyByDay = Array.from({ length: 7 }, () => new Array(24).fill(0));

  let validCount = 0;
  let earliestDate = null;
  let latestDate = null;

  for (const item of items) {
    const dateValue = getField(item, dateField);
    if (!dateValue) continue;

    const d = new Date(dateValue);
    if (isNaN(d.getTime())) continue;

    validCount++;
    hourDistribution[d.getHours()]++;
    dayDistribution[d.getDay()]++;
    hourlyByDay[d.getDay()][d.getHours()]++;

    if (!earliestDate || d < earliestDate) earliestDate = d;
    if (!latestDate || d > latestDate) latestDate = d;
  }

  // Find peaks
  const peakHour = hourDistribution.indexOf(Math.max(...hourDistribution));
  const peakDay = dayDistribution.indexOf(Math.max(...dayDistribution));
  const quietHour = hourDistribution.indexOf(Math.min(...hourDistribution.filter(x => x > 0)) || 0);
  const quietDay = dayDistribution.indexOf(Math.min(...dayDistribution.filter(x => x > 0)) || 0);

  // Categorize time periods
  const morningCount = hourDistribution.slice(6, 12).reduce((a, b) => a + b, 0);
  const afternoonCount = hourDistribution.slice(12, 18).reduce((a, b) => a + b, 0);
  const eveningCount = hourDistribution.slice(18, 22).reduce((a, b) => a + b, 0);
  const nightCount = hourDistribution.slice(22, 24).reduce((a, b) => a + b, 0) +
                     hourDistribution.slice(0, 6).reduce((a, b) => a + b, 0);

  const weekdayCount = dayDistribution.slice(1, 6).reduce((a, b) => a + b, 0);
  const weekendCount = dayDistribution[0] + dayDistribution[6];

  // Build top hours and days
  const topHours = hourDistribution
    .map((count, hour) => ({ hour: HOUR_LABELS[hour], count }))
    .sort((a, b) => b.count - a.count)
    .slice(0, 5);

  const topDays = dayDistribution
    .map((count, day) => ({ day: DAY_NAMES[day], count }))
    .sort((a, b) => b.count - a.count);

  const result = {
    analyzedAt: new Date().toISOString(),
    dateField,
    totalItems: items.length,
    validTimestamps: validCount,
    dateRange: earliestDate && latestDate ? {
      earliest: earliestDate.toISOString(),
      latest: latestDate.toISOString(),
      spanDays: Math.ceil((latestDate - earliestDate) / (24 * 60 * 60 * 1000)),
    } : null,
    hourly: {
      distribution: hourDistribution,
      labels: HOUR_LABELS,
      peakHour: HOUR_LABELS[peakHour],
      peakCount: hourDistribution[peakHour],
      quietHour: HOUR_LABELS[quietHour],
      topHours,
    },
    daily: {
      distribution: dayDistribution,
      labels: DAY_NAMES,
      peakDay: DAY_NAMES[peakDay],
      peakCount: dayDistribution[peakDay],
      quietDay: DAY_NAMES[quietDay],
      topDays,
    },
    periods: {
      morning: { count: morningCount, percent: Math.round(morningCount / validCount * 100) || 0 },
      afternoon: { count: afternoonCount, percent: Math.round(afternoonCount / validCount * 100) || 0 },
      evening: { count: eveningCount, percent: Math.round(eveningCount / validCount * 100) || 0 },
      night: { count: nightCount, percent: Math.round(nightCount / validCount * 100) || 0 },
    },
    weekVsWeekend: {
      weekday: { count: weekdayCount, percent: Math.round(weekdayCount / validCount * 100) || 0 },
      weekend: { count: weekendCount, percent: Math.round(weekendCount / validCount * 100) || 0 },
    },
    patterns: {
      isNightOwl: nightCount > morningCount,
      isEarlyBird: morningCount > eveningCount,
      prefersWeekends: weekendCount / 2 > weekdayCount / 5,
    },
    heatmap: hourlyByDay.map((hours, day) => ({
      day: DAY_NAMES[day],
      hours,
    })),
  };

  // Ensure output directory exists
  const dir = path.dirname(outputPath);
  if (dir && dir !== ".") fs.mkdirSync(dir, { recursive: true });

  fs.writeFileSync(outputPath, JSON.stringify(result, null, 2));

  console.log(`\n✓ Analyzed ${validCount} timestamps`);
  console.log(`  Date range: ${result.dateRange?.spanDays || 0} days`);
  console.log(`  Peak hour: ${result.hourly.peakHour} (${result.hourly.peakCount} occurrences)`);
  console.log(`  Peak day: ${result.daily.peakDay} (${result.daily.peakCount} occurrences)`);
  console.log(`  Patterns: ${result.patterns.isNightOwl ? "Night owl" : result.patterns.isEarlyBird ? "Early bird" : "Balanced"}`);
  console.log(`  Written to: ${outputPath}`);

  console.log(JSON.stringify({
    success: true,
    outputPath,
    validTimestamps: validCount,
    peakHour: result.hourly.peakHour,
    peakDay: result.daily.peakDay,
    patterns: result.patterns,
  }));
} catch (error) {
  console.error("Error:", error.message);
  process.exit(1);
}