Volume 3: Human-System Collaboration

Pattern 16: Temporal Validation

Part II: Interaction Patterns - Temporal Patterns


Opening Scenario: The Meeting That Never Happened

James was scheduling a team meeting using the company's reservation system. He needed to book a conference room for December 25th at 2:00 PM.

Meeting Date: 12/25/2024
Start Time: 2:00 PM
End Time: 3:00 PM
Conference Room: Board Room A

[Submit Reservation]

He clicked Submit. The system accepted it.

December 25th came. James arrived at the office at 1:45 PM. The building was locked. The parking lot was empty.

December 25th was Christmas Day. The office was closed.


The facilities manager, Linda, looked at the reservation logs. "Why did the system let him book a meeting on Christmas?"

She checked the code:

// Bad: Only validates date format
function validateMeetingDate(date) {
  return date instanceof Date && !isNaN(date);
}

// Doesn't check:
// - Is it a holiday?
// - Is it a weekend?
// - Is it in the past?
// - Is the office open?

Linda found more temporal validation failures in the logs:

Meeting scheduled in the past:

Meeting Date: 11/15/2024  ← Last month!
Current Date: 12/10/2024
Status: Accepted  ← Should be rejected

Meeting on Saturday:

Meeting Date: 12/14/2024  ← Saturday
Office Hours: Monday-Friday
Status: Accepted  ← Should be rejected

Meeting outside business hours:

Start Time: 10:00 PM  ← After hours
Office Hours: 8 AM - 6 PM
Status: Accepted  ← Should be rejected

Meeting duration impossible:

Start: 2:00 PM
End: 1:00 PM  ← End before start!
Status: Accepted  ← Obviously wrong

Linda rebuilt the system with intelligent temporal validation:

class TemporalValidator {
  constructor(config) {
    this.holidays = config.holidays || [];
    this.businessDays = config.businessDays || [1,2,3,4,5]; // Mon-Fri
    this.businessHours = config.businessHours || { start: 8, end: 18 };
    this.timezone = config.timezone || 'America/New_York';
  }

  validateMeetingDate(date, now = new Date()) {
    const errors = [];

    // Past date?
    if (date < now) {
      errors.push({
        type: 'past-date',
        message: 'Cannot schedule meetings in the past',
        suggestion: `Select a date on or after ${this.formatDate(now)}`
      });
    }

    // Weekend?
    if (!this.isBusinessDay(date)) {
      errors.push({
        type: 'weekend',
        message: `Office is closed on ${this.getDayName(date)}`,
        suggestion: 'Select a weekday (Monday-Friday)'
      });
    }

    // Holiday?
    if (this.isHoliday(date)) {
      const holiday = this.getHolidayName(date);
      errors.push({
        type: 'holiday',
        message: `Office is closed for ${holiday}`,
        suggestion: this.suggestNextBusinessDay(date)
      });
    }

    // Too far in future?
    const maxAdvance = 365; // Days
    const daysAhead = this.daysBetween(now, date);
    if (daysAhead > maxAdvance) {
      errors.push({
        type: 'too-far-future',
        message: `Cannot book more than ${maxAdvance} days in advance`,
        suggestion: `Select a date within ${maxAdvance} days`
      });
    }

    return {
      valid: errors.length === 0,
      errors: errors
    };
  }

  validateTimeRange(startTime, endTime, date) {
    const errors = [];

    // End before start?
    if (endTime <= startTime) {
      errors.push({
        type: 'invalid-range',
        message: 'End time must be after start time',
        suggestion: 'Adjust your times'
      });
    }

    // During business hours?
    const startHour = this.getHour(startTime);
    const endHour = this.getHour(endTime);

    if (startHour < this.businessHours.start) {
      errors.push({
        type: 'before-hours',
        message: `Office opens at ${this.businessHours.start}:00 AM`,
        suggestion: `Start time must be ${this.businessHours.start}:00 AM or later`
      });
    }

    if (endHour > this.businessHours.end) {
      errors.push({
        type: 'after-hours',
        message: `Office closes at ${this.businessHours.end}:00 PM`,
        suggestion: `End time must be before ${this.businessHours.end}:00 PM`
      });
    }

    // Too long?
    const duration = (endTime - startTime) / (1000 * 60 * 60); // Hours
    if (duration > 4) {
      errors.push({
        type: 'too-long',
        message: 'Meetings cannot exceed 4 hours',
        severity: 'warning'
      });
    }

    return {
      valid: errors.length === 0,
      errors: errors
    };
  }

  isBusinessDay(date) {
    const day = date.getDay();
    return this.businessDays.includes(day);
  }

  isHoliday(date) {
    const dateStr = this.formatDate(date);
    return this.holidays.some(h => h.date === dateStr);
  }

  getHolidayName(date) {
    const dateStr = this.formatDate(date);
    const holiday = this.holidays.find(h => h.date === dateStr);
    return holiday ? holiday.name : 'Holiday';
  }

  suggestNextBusinessDay(date) {
    let next = new Date(date);
    next.setDate(next.getDate() + 1);

    while (!this.isBusinessDay(next) || this.isHoliday(next)) {
      next.setDate(next.getDate() + 1);
    }

    return `Try ${this.formatDate(next)} instead`;
  }
}

Now when James tried to book December 25th:

Meeting Date: 12/25/2024

❌ Office is closed for Christmas Day
💡 Try 12/26/2024 instead

[The date field is highlighted with this error]

When someone tried to book Saturday:

Meeting Date: 12/14/2024

❌ Office is closed on Saturday
💡 Select a weekday (Monday-Friday)

Quick suggestions:
[Friday 12/13] [Monday 12/16]

When someone tried to schedule 10 PM:

Start Time: 10:00 PM

❌ Office closes at 6:00 PM
💡 End time must be before 6:00 PM

Suggested times within business hours:
[2:00 PM] [3:00 PM] [4:00 PM] [5:00 PM]

Meeting reservation errors dropped to zero. Users got immediate feedback. And James never showed up to a locked office again.

Context

Temporal Validation applies when:

Time matters: Dates, times, durations are critical to validity

Business hours exist: Operations have temporal constraints

Calendars are complex: Weekends, holidays, special dates matter

Ranges must be logical: Start before end, duration limits

Time zones are relevant: Users in different locations

Scheduling is involved: Appointments, reservations, deadlines

Temporal logic is complex: Business rules about when things can happen

Problem Statement

Most forms validate dates as mere syntax, missing temporal logic and business rules:

Format-only validation:

// Bad: Only checks if it's a valid date
function validateDate(date) {
  return date instanceof Date && !isNaN(date);
}

// Accepts:
// - Dates in the past (when future required)
// - Holidays and weekends
// - Dates 100 years in the future
// - Any date that parses

No business day awareness:

// Bad: Doesn't know about weekends
function validateAppointment(date) {
  return date > new Date();
}

// Accepts Saturday/Sunday appointments
// Even though office is closed

No holiday checking:

// Bad: No holiday calendar
function isAvailable(date) {
  const day = date.getDay();
  return day !== 0 && day !== 6; // Not Sunday or Saturday
}

// But what about:
// - Christmas
// - Thanksgiving
// - Company holidays
// - Regional observances

No time range logic:

// Bad: Doesn't validate time relationship
function validateTimes(start, end) {
  return start && end; // Just checks they exist!
}

// Allows:
// - End time before start time
// - Midnight meetings
// - 24-hour durations

No timezone awareness:

// Bad: Assumes user's timezone
function scheduleCall(time) {
  return time; // What timezone?
}

// User in California, meeting in New York
// 2 PM becomes 5 PM, nobody knows

No context about purpose:

// Bad: Same validation for all date fields
function validateDate(date) {
  return date > new Date();
}

// But:
// - Birth dates should be PAST
// - Appointments should be FUTURE
// - Invoice dates might be PAST or PRESENT

We need validation that understands time, calendars, business rules, and context.

Forces

Precision vs Flexibility

  • Strict temporal rules prevent errors
  • But may block legitimate edge cases
  • Need override mechanisms

Local vs Universal

  • Holidays vary by location
  • Business hours differ by office
  • One size doesn't fit all

Real-time vs Static

  • Current time constantly changes
  • Validation results can shift
  • Need consistency in logic

User Time vs System Time

  • Users operate in their timezone
  • System may be elsewhere
  • Clear timezone handling essential

Simplicity vs Completeness

  • Complete temporal logic is complex
  • But partial validation misses errors
  • Balance thoroughness with usability

Solution

Implement comprehensive temporal validation that understands business calendars, time zones, date ranges, and contextual temporal rules - providing clear feedback about why dates are invalid and suggesting valid alternatives.

The pattern has six key strategies:

1. Business Calendar Definition

Define organizational temporal rules:

class BusinessCalendar {
  constructor(config) {
    this.timezone = config.timezone || 'UTC';
    this.businessDays = config.businessDays || [1,2,3,4,5];
    this.businessHours = config.businessHours || { start: 9, end: 17 };
    this.holidays = this.loadHolidays(config.holidays);
    this.specialDates = config.specialDates || [];
  }

  loadHolidays(holidayConfig) {
    // Fixed holidays
    const fixed = [
      { month: 1, day: 1, name: "New Year's Day" },
      { month: 7, day: 4, name: "Independence Day" },
      { month: 12, day: 25, name: "Christmas Day" }
    ];

    // Floating holidays (calculated)
    const floating = this.calculateFloatingHolidays(new Date().getFullYear());

    return [...fixed, ...floating, ...(holidayConfig || [])];
  }

  calculateFloatingHolidays(year) {
    return [
      this.getNthWeekdayOfMonth(year, 11, 1, 4), // Thanksgiving (4th Thurs of Nov)
      this.getNthWeekdayOfMonth(year, 9, 1, 1),  // Labor Day (1st Mon of Sept)
      this.getNthWeekdayOfMonth(year, 5, 1, -1)  // Memorial Day (last Mon of May)
    ];
  }

  getNthWeekdayOfMonth(year, month, weekday, n) {
    // Get the nth occurrence of weekday in month
    // n = -1 means last occurrence
    const date = new Date(year, month, 1);
    const days = [];

    while (date.getMonth() === month) {
      if (date.getDay() === weekday) {
        days.push(new Date(date));
      }
      date.setDate(date.getDate() + 1);
    }

    if (n === -1) {
      return {
        date: this.formatDate(days[days.length - 1]),
        name: 'Memorial Day'
      };
    }

    return {
      date: this.formatDate(days[n - 1]),
      name: 'Thanksgiving'
    };
  }

  isBusinessDay(date) {
    // Weekend?
    if (!this.businessDays.includes(date.getDay())) {
      return false;
    }

    // Holiday?
    if (this.isHoliday(date)) {
      return false;
    }

    // Special closure?
    if (this.isSpecialClosure(date)) {
      return false;
    }

    return true;
  }

  isHoliday(date) {
    const dateStr = this.formatDate(date);
    return this.holidays.some(h => h.date === dateStr);
  }

  isSpecialClosure(date) {
    const dateStr = this.formatDate(date);
    return this.specialDates.some(s => 
      s.date === dateStr && s.type === 'closure'
    );
  }

  getNextBusinessDay(fromDate) {
    let next = new Date(fromDate);
    next.setDate(next.getDate() + 1);

    while (!this.isBusinessDay(next)) {
      next.setDate(next.getDate() + 1);
    }

    return next;
  }

  getPreviousBusinessDay(fromDate) {
    let prev = new Date(fromDate);
    prev.setDate(prev.getDate() - 1);

    while (!this.isBusinessDay(prev)) {
      prev.setDate(prev.getDate() - 1);
    }

    return prev;
  }

  getBusinessDaysBetween(start, end) {
    let count = 0;
    let current = new Date(start);

    while (current <= end) {
      if (this.isBusinessDay(current)) {
        count++;
      }
      current.setDate(current.getDate() + 1);
    }

    return count;
  }

  addBusinessDays(fromDate, days) {
    let result = new Date(fromDate);
    let remaining = days;

    while (remaining > 0) {
      result.setDate(result.getDate() + 1);
      if (this.isBusinessDay(result)) {
        remaining--;
      }
    }

    return result;
  }

  formatDate(date) {
    return date.toISOString().split('T')[0];
  }
}

2. Temporal Range Validation

Validate date/time relationships:

class TemporalRangeValidator {
  constructor(calendar) {
    this.calendar = calendar;
  }

  validateDateRange(startDate, endDate, config = {}) {
    const errors = [];

    // Start after end?
    if (endDate < startDate) {
      errors.push({
        type: 'invalid-range',
        message: 'End date must be after start date',
        fields: ['startDate', 'endDate']
      });
      return { valid: false, errors };
    }

    // Duration limits
    if (config.maxDuration) {
      const days = this.daysBetween(startDate, endDate);
      if (days > config.maxDuration) {
        errors.push({
          type: 'too-long',
          message: `Duration cannot exceed ${config.maxDuration} days`,
          actual: days,
          max: config.maxDuration
        });
      }
    }

    if (config.minDuration) {
      const days = this.daysBetween(startDate, endDate);
      if (days < config.minDuration) {
        errors.push({
          type: 'too-short',
          message: `Duration must be at least ${config.minDuration} days`,
          actual: days,
          min: config.minDuration
        });
      }
    }

    // Must span business days?
    if (config.requireBusinessDays) {
      const businessDays = this.calendar.getBusinessDaysBetween(startDate, endDate);
      if (businessDays === 0) {
        errors.push({
          type: 'no-business-days',
          message: 'Date range must include at least one business day',
          suggestion: this.suggestRangeWithBusinessDays(startDate, endDate)
        });
      }
    }

    return {
      valid: errors.length === 0,
      errors: errors,
      businessDays: this.calendar.getBusinessDaysBetween(startDate, endDate),
      totalDays: this.daysBetween(startDate, endDate)
    };
  }

  validateTimeRange(startTime, endTime, config = {}) {
    const errors = [];

    // End before start?
    if (endTime <= startTime) {
      errors.push({
        type: 'invalid-time-range',
        message: 'End time must be after start time'
      });
      return { valid: false, errors };
    }

    // Within business hours?
    if (config.businessHoursOnly) {
      const startHour = startTime.getHours();
      const endHour = endTime.getHours();

      if (startHour < this.calendar.businessHours.start) {
        errors.push({
          type: 'before-business-hours',
          message: `Start time must be after ${this.calendar.businessHours.start}:00`,
          suggestion: `Earliest start: ${this.calendar.businessHours.start}:00`
        });
      }

      if (endHour > this.calendar.businessHours.end) {
        errors.push({
          type: 'after-business-hours',
          message: `End time must be before ${this.calendar.businessHours.end}:00`,
          suggestion: `Latest end: ${this.calendar.businessHours.end}:00`
        });
      }
    }

    // Duration limits
    const minutes = (endTime - startTime) / (1000 * 60);

    if (config.maxMinutes && minutes > config.maxMinutes) {
      errors.push({
        type: 'duration-too-long',
        message: `Duration cannot exceed ${config.maxMinutes} minutes`,
        actual: minutes
      });
    }

    if (config.minMinutes && minutes < config.minMinutes) {
      errors.push({
        type: 'duration-too-short',
        message: `Duration must be at least ${config.minMinutes} minutes`,
        actual: minutes
      });
    }

    return {
      valid: errors.length === 0,
      errors: errors,
      duration: minutes
    };
  }

  daysBetween(start, end) {
    const oneDay = 1000 * 60 * 60 * 24;
    return Math.round((end - start) / oneDay);
  }
}

3. Contextual Temporal Rules

Different validation based on purpose:

class ContextualTemporalRules {
  constructor(calendar) {
    this.calendar = calendar;
    this.rules = new Map();
  }

  defineRule(fieldName, context, rule) {
    const key = `${fieldName}:${context}`;
    this.rules.set(key, rule);
  }

  // Define common temporal patterns
  defineCommonRules() {
    // Birth dates: must be past, reasonable age range
    this.defineRule('birthDate', 'person', {
      direction: 'past',
      minYearsAgo: 0,
      maxYearsAgo: 120,
      canBeToday: true
    });

    // Appointment dates: must be future, business days only
    this.defineRule('appointmentDate', 'scheduling', {
      direction: 'future',
      businessDaysOnly: true,
      minDaysAhead: 0,
      maxDaysAhead: 365,
      excludeHolidays: true
    });

    // Invoice dates: can be past or present, not future
    this.defineRule('invoiceDate', 'billing', {
      direction: 'past-or-present',
      maxDaysAgo: 90,
      canBeToday: true
    });

    // Event start: must be future, reasonable advance booking
    this.defineRule('eventStart', 'event', {
      direction: 'future',
      minDaysAhead: 7,
      maxDaysAhead: 730, // 2 years
      businessDaysOnly: false
    });

    // Deadline: must be future, urgency levels
    this.defineRule('deadline', 'task', {
      direction: 'future',
      minDaysAhead: 1,
      warnings: {
        urgent: 3,    // Warn if less than 3 days
        soon: 7       // Note if less than 7 days
      }
    });
  }

  validate(fieldName, value, context, referenceDate = new Date()) {
    const key = `${fieldName}:${context}`;
    const rule = this.rules.get(key);

    if (!rule) {
      return { valid: true }; // No rule defined
    }

    const errors = [];
    const warnings = [];

    // Direction validation
    if (rule.direction === 'future' && value <= referenceDate) {
      errors.push({
        type: 'must-be-future',
        message: `${fieldName} must be in the future`,
        suggestion: this.suggestFutureDate(referenceDate, rule)
      });
    }

    if (rule.direction === 'past' && value >= referenceDate) {
      errors.push({
        type: 'must-be-past',
        message: `${fieldName} must be in the past`
      });
    }

    if (rule.direction === 'past-or-present' && value > referenceDate) {
      errors.push({
        type: 'cannot-be-future',
        message: `${fieldName} cannot be in the future`
      });
    }

    // Business days only?
    if (rule.businessDaysOnly && !this.calendar.isBusinessDay(value)) {
      errors.push({
        type: 'not-business-day',
        message: this.getBusinessDayError(value),
        suggestion: this.calendar.getNextBusinessDay(value)
      });
    }

    // Exclude holidays?
    if (rule.excludeHolidays && this.calendar.isHoliday(value)) {
      errors.push({
        type: 'is-holiday',
        message: `${this.calendar.getHolidayName(value)} - Office closed`,
        suggestion: this.calendar.getNextBusinessDay(value)
      });
    }

    // Minimum days ahead?
    if (rule.minDaysAhead) {
      const daysAhead = this.daysBetween(referenceDate, value);
      if (daysAhead < rule.minDaysAhead) {
        errors.push({
          type: 'too-soon',
          message: `Must be at least ${rule.minDaysAhead} days in advance`,
          actual: daysAhead,
          required: rule.minDaysAhead
        });
      }
    }

    // Maximum days ahead?
    if (rule.maxDaysAhead) {
      const daysAhead = this.daysBetween(referenceDate, value);
      if (daysAhead > rule.maxDaysAhead) {
        errors.push({
          type: 'too-far-future',
          message: `Cannot be more than ${rule.maxDaysAhead} days in advance`,
          actual: daysAhead,
          max: rule.maxDaysAhead
        });
      }
    }

    // Warnings (urgency, etc.)
    if (rule.warnings) {
      const daysAhead = this.daysBetween(referenceDate, value);

      if (daysAhead <= rule.warnings.urgent) {
        warnings.push({
          type: 'urgent',
          message: `Only ${daysAhead} days until deadline`,
          severity: 'high'
        });
      } else if (daysAhead <= rule.warnings.soon) {
        warnings.push({
          type: 'soon',
          message: `${daysAhead} days until deadline`,
          severity: 'medium'
        });
      }
    }

    return {
      valid: errors.length === 0,
      errors: errors,
      warnings: warnings
    };
  }

  getBusinessDayError(date) {
    const day = date.getDay();
    if (day === 0) return 'Office closed on Sunday';
    if (day === 6) return 'Office closed on Saturday';
    return 'Not a business day';
  }

  daysBetween(start, end) {
    const oneDay = 1000 * 60 * 60 * 24;
    return Math.floor((end - start) / oneDay);
  }
}

4. Timezone Handling

Manage time across zones:

class TimezoneHandler {
  constructor() {
    this.userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
  }

  // Convert user's local time to target timezone
  convertToTimezone(date, targetTimezone) {
    const options = {
      timeZone: targetTimezone,
      year: 'numeric',
      month: '2-digit',
      day: '2-digit',
      hour: '2-digit',
      minute: '2-digit',
      second: '2-digit',
      hour12: false
    };

    const formatter = new Intl.DateTimeFormat('en-US', options);
    return formatter.format(date);
  }

  // Show time in multiple zones
  showInMultipleZones(date, zones) {
    const times = {};

    zones.forEach(zone => {
      times[zone] = this.convertToTimezone(date, zone);
    });

    return times;
  }

  // Validate scheduling across timezones
  validateCrossTimezoneScheduling(meetingTime, attendees) {
    const issues = [];

    attendees.forEach(attendee => {
      const localTime = this.convertToTimezone(meetingTime, attendee.timezone);
      const hour = new Date(localTime).getHours();

      // Outside reasonable meeting hours (7 AM - 10 PM)?
      if (hour < 7 || hour > 22) {
        issues.push({
          attendee: attendee.name,
          timezone: attendee.timezone,
          localTime: localTime,
          issue: `${hour < 7 ? 'Too early' : 'Too late'} (${hour}:00 local time)`
        });
      }
    });

    return {
      valid: issues.length === 0,
      issues: issues
    };
  }

  suggestBestTime(attendees, duration) {
    // Find time that works for all timezones
    // (All attendees between 9 AM - 5 PM local time)

    const possibleTimes = [];

    for (let hour = 0; hour < 24; hour++) {
      const baseTime = new Date();
      baseTime.setHours(hour, 0, 0, 0);

      const worksForAll = attendees.every(attendee => {
        const localTime = this.convertToTimezone(baseTime, attendee.timezone);
        const localHour = new Date(localTime).getHours();
        return localHour >= 9 && localHour <= 17;
      });

      if (worksForAll) {
        possibleTimes.push({
          utcTime: hour,
          localTimes: this.showInMultipleZones(baseTime, 
            attendees.map(a => a.timezone))
        });
      }
    }

    return possibleTimes;
  }
}

5. Temporal Suggestions

Suggest valid alternatives:

class TemporalSuggester {
  constructor(calendar, rules) {
    this.calendar = calendar;
    this.rules = rules;
  }

  suggestAlternatives(invalidDate, context) {
    const suggestions = [];

    // If it's a weekend, suggest nearby weekdays
    if (!this.calendar.isBusinessDay(invalidDate)) {
      const nextBusiness = this.calendar.getNextBusinessDay(invalidDate);
      const prevBusiness = this.calendar.getPreviousBusinessDay(invalidDate);

      suggestions.push({
        date: nextBusiness,
        label: this.formatSuggestion(nextBusiness),
        reason: 'Next business day'
      });

      suggestions.push({
        date: prevBusiness,
        label: this.formatSuggestion(prevBusiness),
        reason: 'Previous business day'
      });
    }

    // If it's in the past, suggest next week
    if (invalidDate < new Date()) {
      const nextWeek = new Date();
      nextWeek.setDate(nextWeek.getDate() + 7);

      // Ensure it's a business day
      if (!this.calendar.isBusinessDay(nextWeek)) {
        nextWeek = this.calendar.getNextBusinessDay(nextWeek);
      }

      suggestions.push({
        date: nextWeek,
        label: this.formatSuggestion(nextWeek),
        reason: 'One week from today'
      });
    }

    // Context-specific suggestions
    if (context === 'appointment') {
      // Suggest common appointment slots
      const tomorrow = new Date();
      tomorrow.setDate(tomorrow.getDate() + 1);

      if (this.calendar.isBusinessDay(tomorrow)) {
        suggestions.push({
          date: tomorrow,
          label: 'Tomorrow',
          times: ['9:00 AM', '2:00 PM', '4:00 PM']
        });
      }
    }

    return suggestions;
  }

  formatSuggestion(date) {
    const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
    const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

    return `${days[date.getDay()]}, ${months[date.getMonth()]} ${date.getDate()}`;
  }

  suggestTimeslots(date, duration, existing = []) {
    const slots = [];
    const businessHours = this.calendar.businessHours;

    // Generate 30-minute slots within business hours
    let hour = businessHours.start;
    let minute = 0;

    while (hour < businessHours.end) {
      const slotStart = new Date(date);
      slotStart.setHours(hour, minute, 0);

      const slotEnd = new Date(slotStart);
      slotEnd.setMinutes(slotEnd.getMinutes() + duration);

      // Check if slot conflicts with existing appointments
      const conflicts = this.checkConflicts(slotStart, slotEnd, existing);

      if (!conflicts) {
        slots.push({
          start: slotStart,
          end: slotEnd,
          label: this.formatTime(slotStart)
        });
      }

      // Next slot
      minute += 30;
      if (minute >= 60) {
        minute = 0;
        hour++;
      }
    }

    return slots;
  }

  checkConflicts(start, end, existing) {
    return existing.some(appt => {
      return (start < appt.end && end > appt.start);
    });
  }

  formatTime(date) {
    let hours = date.getHours();
    const minutes = date.getMinutes();
    const ampm = hours >= 12 ? 'PM' : 'AM';

    hours = hours % 12;
    hours = hours ? hours : 12;

    const minutesStr = minutes < 10 ? '0' + minutes : minutes;

    return `${hours}:${minutesStr} ${ampm}`;
  }
}

6. Visual Temporal Feedback

Show temporal context visually:

class TemporalFeedbackUI {
  constructor(calendar) {
    this.calendar = calendar;
  }

  enhanceDateInput(inputElement, context) {
    // Add calendar icon with context
    const icon = document.createElement('span');
    icon.className = 'date-input-icon';
    icon.innerHTML = '📅';

    // Add mini calendar popup
    inputElement.addEventListener('focus', () => {
      this.showContextualCalendar(inputElement, context);
    });

    // Color-code the input based on validation
    inputElement.addEventListener('change', () => {
      this.validateAndColorCode(inputElement, context);
    });
  }

  showContextualCalendar(inputElement, context) {
    const calendar = this.renderCalendar(context);

    // Position near input
    const rect = inputElement.getBoundingClientRect();
    calendar.style.top = rect.bottom + 'px';
    calendar.style.left = rect.left + 'px';

    document.body.appendChild(calendar);
  }

  renderCalendar(context) {
    const calendar = document.createElement('div');
    calendar.className = 'contextual-calendar';

    const today = new Date();
    const month = today.getMonth();
    const year = today.getFullYear();

    // Render calendar grid
    calendar.innerHTML = this.renderMonthGrid(year, month, context);

    return calendar;
  }

  renderMonthGrid(year, month, context) {
    const firstDay = new Date(year, month, 1);
    const lastDay = new Date(year, month + 1, 0);
    const daysInMonth = lastDay.getDate();

    let html = '<div class="calendar-grid">';

    // Day headers
    html += '<div class="day-header">Sun</div>';
    html += '<div class="day-header">Mon</div>';
    html += '<div class="day-header">Tue</div>';
    html += '<div class="day-header">Wed</div>';
    html += '<div class="day-header">Thu</div>';
    html += '<div class="day-header">Fri</div>';
    html += '<div class="day-header">Sat</div>';

    // Blank cells for first week
    const startDay = firstDay.getDay();
    for (let i = 0; i < startDay; i++) {
      html += '<div class="day-cell empty"></div>';
    }

    // Days of month
    for (let day = 1; day <= daysInMonth; day++) {
      const date = new Date(year, month, day);
      const classes = this.getDayClasses(date, context);

      html += `<div class="day-cell ${classes}" data-date="${date.toISOString()}">`;
      html += `<span class="day-number">${day}</span>`;

      // Add indicators
      if (this.calendar.isHoliday(date)) {
        html += '<span class="holiday-indicator">🎉</span>';
      }

      html += '</div>';
    }

    html += '</div>';

    return html;
  }

  getDayClasses(date, context) {
    const classes = [];

    // Today
    const today = new Date();
    if (this.isSameDay(date, today)) {
      classes.push('today');
    }

    // Past
    if (date < today) {
      classes.push('past');
    }

    // Weekend
    const day = date.getDay();
    if (day === 0 || day === 6) {
      classes.push('weekend');
    }

    // Holiday
    if (this.calendar.isHoliday(date)) {
      classes.push('holiday');
    }

    // Not a business day
    if (!this.calendar.isBusinessDay(date)) {
      classes.push('not-business-day');
    }

    // Context-specific
    if (context === 'appointment' && !this.calendar.isBusinessDay(date)) {
      classes.push('disabled');
    }

    return classes.join(' ');
  }

  validateAndColorCode(inputElement, context) {
    const value = inputElement.value;
    if (!value) return;

    const date = new Date(value);
    const validation = this.rules.validate(
      inputElement.name,
      date,
      context
    );

    // Remove previous classes
    inputElement.classList.remove('valid', 'invalid', 'warning');

    if (!validation.valid) {
      inputElement.classList.add('invalid');
      this.showInlineError(inputElement, validation.errors[0]);
    } else if (validation.warnings && validation.warnings.length > 0) {
      inputElement.classList.add('warning');
      this.showInlineWarning(inputElement, validation.warnings[0]);
    } else {
      inputElement.classList.add('valid');
      this.clearInlineMessages(inputElement);
    }
  }

  showInlineError(inputElement, error) {
    const container = inputElement.closest('.field-container');
    let errorDiv = container.querySelector('.inline-error');

    if (!errorDiv) {
      errorDiv = document.createElement('div');
      errorDiv.className = 'inline-error';
      container.appendChild(errorDiv);
    }

    errorDiv.innerHTML = `
      <span class="icon">❌</span>
      <span class="message">${error.message}</span>
      ${error.suggestion ? `
        <button class="suggestion-btn" onclick="applySuggestion('${error.suggestion}')">
          ${error.suggestion}
        </button>
      ` : ''}
    `;
  }

  isSameDay(date1, date2) {
    return date1.getFullYear() === date2.getFullYear() &&
           date1.getMonth() === date2.getMonth() &&
           date1.getDate() === date2.getDate();
  }
}

Implementation Details

Complete Temporal Validation System

class TemporalValidationSystem {
  constructor(config) {
    this.calendar = new BusinessCalendar(config.calendar);
    this.rangeValidator = new TemporalRangeValidator(this.calendar);
    this.rules = new ContextualTemporalRules(this.calendar);
    this.timezone = new TimezoneHandler();
    this.suggester = new TemporalSuggester(this.calendar, this.rules);
    this.ui = new TemporalFeedbackUI(this.calendar);

    // Define standard rules
    this.rules.defineCommonRules();

    // Add custom rules
    if (config.customRules) {
      config.customRules.forEach(rule => {
        this.rules.defineRule(rule.field, rule.context, rule.validation);
      });
    }
  }

  validateField(fieldName, value, context) {
    // Get validation result
    const validation = this.rules.validate(fieldName, value, context);

    // Add suggestions if invalid
    if (!validation.valid) {
      validation.suggestions = this.suggester.suggestAlternatives(value, context);
    }

    return validation;
  }

  validateRange(startField, endField, values, config) {
    return this.rangeValidator.validateDateRange(
      values[startField],
      values[endField],
      config
    );
  }

  setupFormEnhancements(formElement) {
    // Find all date/time inputs
    const dateInputs = formElement.querySelectorAll('input[type="date"], input[type="datetime-local"]');

    dateInputs.forEach(input => {
      const context = input.dataset.context || 'default';
      this.ui.enhanceDateInput(input, context);
    });
  }
}

// Example usage
const temporalSystem = new TemporalValidationSystem({
  calendar: {
    timezone: 'America/New_York',
    businessDays: [1, 2, 3, 4, 5],  // Mon-Fri
    businessHours: { start: 9, end: 17 },
    holidays: [
      { month: 12, day: 25, name: "Christmas" },
      { month: 7, day: 4, name: "Independence Day" }
    ]
  },
  customRules: [
    {
      field: 'deliveryDate',
      context: 'shipping',
      validation: {
        direction: 'future',
        minDaysAhead: 2,  // 2-day minimum for processing
        businessDaysOnly: true
      }
    }
  ]
});

// Validate a date
const result = temporalSystem.validateField(
  'appointmentDate',
  new Date('2024-12-25'),
  'scheduling'
);

if (!result.valid) {
  console.log('Error:', result.errors[0].message);
  console.log('Try:', result.suggestions[0].label);
}

Consequences

Benefits

Prevents Temporal Errors: - No meetings on holidays - No past-dated appointments - No invalid time ranges - Business rules enforced

Better User Experience: - Clear feedback about why dates invalid - Suggestions for valid alternatives - Visual calendar shows constraints - Timezone awareness

Reduced Support Burden: - Self-explanatory temporal rules - Users understand constraints - Fewer booking errors

Business Logic Encoded: - Holidays defined once - Business hours enforced - Organizational rules preserved

Timezone Handling: - Cross-timezone scheduling works - Clear time conversion - No confusion about "when"

Liabilities

Complexity: - Temporal logic is intricate - Many edge cases - Testing challenging

Calendar Maintenance: - Holidays change yearly - Special closures must be updated - Regional variations

Performance: - Date calculations expensive - Calendar lookups add overhead - Need caching strategies

Rigidity: - May block legitimate edge cases - Emergency overrides needed - Balance rules with flexibility

Cultural Variations: - Holidays vary by location - Calendar systems differ - Week start varies

Domain Examples

Healthcare: Appointment Scheduling

// Medical appointment temporal rules
defineAppointmentRules() {
  this.rules.defineRule('appointmentDate', 'medical', {
    direction: 'future',
    businessDaysOnly: true,
    minDaysAhead: 0,  // Same-day allowed for urgent
    maxDaysAhead: 90,
    excludeHolidays: true,
    specialRules: {
      'urgent': { minDaysAhead: 0 },
      'routine': { minDaysAhead: 7 },
      'followup': { minDaysAhead: 30 }
    }
  });
}

Financial: Transaction Dating

// Banking transaction temporal rules
defineTransactionRules() {
  this.rules.defineRule('transactionDate', 'banking', {
    direction: 'past-or-present',
    maxDaysAgo: 90,
    businessDaysOnly: false,  // ATM works weekends
    warnings: {
      old: 30  // Warn if > 30 days ago
    }
  });
}
// Court filing temporal rules
defineFilingRules() {
  this.rules.defineRule('filingDeadline', 'legal', {
    direction: 'future',
    businessDaysOnly: true,
    minDaysAhead: 1,
    courtHoursOnly: true,
    calculateDeadline: (fromDate, days) => {
      // Add business days only
      return this.calendar.addBusinessDays(fromDate, days);
    }
  });
}

Prerequisites: - Volume 3, Pattern 6: Domain-Aware Validation (validation framework) - Volume 3, Pattern 9: Contextual Constraints (context affects validity)

Synergies: - Volume 3, Pattern 10: Semantic Suggestions (suggest valid dates) - Volume 3, Pattern 14: Cross-Field Validation (date ranges) - Volume 3, Pattern 17: State-Aware Behavior (temporal state)

Conflicts: - Extreme flexibility requirements - Global applications (too many calendar variations)

Alternatives: - Manual calendar checking (user responsibility) - Server-side only (check at submission) - Simplified rules (weekdays only, no holidays)

Known Uses

Booking Systems (Airbnb, Hotels): Check-in/out dates, availability calendars

Scheduling Apps (Calendly, Doodle): Business hours, timezone handling

Project Management (Asana, Jira): Due dates, sprint planning, milestones

HR Systems: PTO requests, performance review cycles, hiring timelines

Financial Software: Transaction dating, statement periods, tax deadlines

Travel Booking: Departure/arrival dates, visa validity, travel restrictions

Healthcare Portals: Appointment scheduling, prescription refills, test results


Further Reading

Academic Foundations

  • Temporal Logic: Emerson, E.A. (1990). "Temporal and Modal Logic." Handbook of Theoretical Computer Science Vol. B: 995-1072.
  • Date/Time Algorithms: Dershowitz, N., & Reingold, E.M. (2008). Calendrical Calculations (3rd ed.). Cambridge University Press. ISBN: 978-0521885409
  • Timezone Handling: Klyne, G., & Newman, C. (2002). "Date and Time on the Internet: Timestamps" RFC 3339. https://tools.ietf.org/html/rfc3339

Practical Implementation

Standards & Specifications

  • Pattern 11: Validation Rules - Temporal rules are validation
  • Pattern 19: Cross-Field Validation - Date range validation
  • Pattern 23: Audit Trail - Timestamp all changes
  • Pattern 26: External Data Integration - Holiday/calendar APIs
  • Volume 2, Pattern 14: Predictive Time Windows - Time-based predictions
  • Volume 1, Chapter 6: Business Domain Patterns - Temporal coordination

Tools & Services

Implementation Examples