gitshark

Clone repository

git clone https://git.vilip.de/git/vilip/taskflow.git
git clone git@git.vilip.de:vilip/taskflow.git
/**
 * Task recurrence helpers
 *
 * Pure, platform-independent logic for repeating tasks so the scheduling math
 * can be unit-tested without storage or UI.
 */

export type RecurrenceKind = 'weekly' | 'monthly' | 'everyNDays' | 'weekdays';

export interface Recurrence {
  kind: RecurrenceKind;
  /** For 'everyNDays': repeat every N days (>= 1). */
  intervalDays?: number;
  /** For 'weekdays': days of week to repeat on, 0 = Sunday … 6 = Saturday. */
  weekdays?: number[];
}

/** Short weekday labels, indexed by Date.getDay() (0 = Sunday). */
export const WEEKDAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];

/** Validate that a recurrence is internally consistent. */
export function isValidRecurrence(recurrence: Recurrence): boolean {
  switch (recurrence.kind) {
    case 'weekly':
    case 'monthly':
      return true;
    case 'everyNDays':
      return Number.isInteger(recurrence.intervalDays) && (recurrence.intervalDays as number) >= 1;
    case 'weekdays':
      return (
        Array.isArray(recurrence.weekdays) &&
        recurrence.weekdays.length > 0 &&
        recurrence.weekdays.every((d) => Number.isInteger(d) && d >= 0 && d <= 6)
      );
    default:
      return false;
  }
}

/**
 * Compute the next due date (timestamp) strictly after `from`, preserving the
 * time of day.
 */
export function computeNextDueDate(from: number, recurrence: Recurrence): number {
  const date = new Date(from);
  switch (recurrence.kind) {
    case 'weekly':
      date.setDate(date.getDate() + 7);
      return date.getTime();
    case 'everyNDays':
      date.setDate(date.getDate() + (recurrence.intervalDays ?? 1));
      return date.getTime();
    case 'monthly':
      date.setMonth(date.getMonth() + 1);
      return date.getTime();
    case 'weekdays': {
      const days = new Set(recurrence.weekdays ?? []);
      for (let offset = 1; offset <= 7; offset++) {
        const candidate = new Date(from);
        candidate.setDate(candidate.getDate() + offset);
        if (days.has(candidate.getDay())) {
          return candidate.getTime();
        }
      }
      // Fallback (only reachable with an invalid empty weekday set).
      date.setDate(date.getDate() + 7);
      return date.getTime();
    }
  }
}

/** Human-readable description of a recurrence, e.g. "Weekly", "Every 3 days". */
export function describeRecurrence(recurrence: Recurrence): string {
  switch (recurrence.kind) {
    case 'weekly':
      return 'Weekly';
    case 'monthly':
      return 'Monthly';
    case 'everyNDays': {
      const n = recurrence.intervalDays ?? 1;
      return n === 1 ? 'Every day' : `Every ${n} days`;
    }
    case 'weekdays': {
      const labels = (recurrence.weekdays ?? [])
        .slice()
        .sort((a, b) => a - b)
        .map((d) => WEEKDAY_LABELS[d]);
      return labels.join(', ');
    }
    default:
      return '';
  }
}

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog