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
}
});
}
Legal: Filing Deadlines
// 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);
}
});
}
Related Patterns
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
- date-fns: https://date-fns.org/ - Modern JavaScript date utility library
- Luxon: https://moment.github.io/luxon/ - DateTime library with timezone support
- Day.js: https://day.js.org/ - Lightweight alternative to Moment.js
- Temporal API: https://tc39.es/proposal-temporal/docs/ - Modern JavaScript date/time (Stage 3)
- Chrono: https://github.com/wanasit/chrono - Natural language date parsing
Standards & Specifications
- ISO 8601: https://www.iso.org/iso-8601-date-and-time-format.html - Date and time format standard
- IANA Time Zone Database: https://www.iana.org/time-zones - Authoritative timezone data
- RFC 5545 (iCalendar): https://tools.ietf.org/html/rfc5545 - Calendar data format
Related Trilogy Patterns
- 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
- Moment Timezone: https://momentjs.com/timezone/ - Timezone data and conversion
- Business Day Calculator APIs: https://www.calendarific.com/api-documentation - Holidays/business days
- Google Calendar API: https://developers.google.com/calendar - Calendar integration
- When2Meet: https://www.when2meet.com/ - Scheduling across timezones
Implementation Examples
- Date Validation Patterns: https://www.nngroup.com/articles/date-input/
- Timezone Best Practices: https://www.w3.org/International/wiki/WorkingWithTimeZones
- Calendar Picker Design: https://uxdesign.cc/date-picker-design-best-practices-41c19c4f1e32