@codecademy/gamut

72.2.272.2.3-alpha.303f64.0
dist/DatePicker/DatePickerInput/utils/validation.js
+dist/DatePicker/DatePickerInput/utils/validation.jsNew file
+146
Index: package/dist/DatePicker/DatePickerInput/utils/validation.js
===================================================================
--- package/dist/DatePicker/DatePickerInput/utils/validation.js
+++ package/dist/DatePicker/DatePickerInput/utils/validation.js
@@ -0,0 +1,146 @@
+import { parseSegmentsToDate } from '../Segment/utils';
+/**
+ * Detects the specific reason why a date is invalid.
+ * Assumes segments are "complete enough" (all three fields have at least some input).
+ */
+function detectValidationIssue(segments) {
+  const {
+    month,
+    day,
+    year
+  } = segments;
+
+  // Parse to numbers for validation
+  const monthNumber = parseInt(month, 10);
+  const dayNumber = parseInt(day, 10);
+  const yearNumber = parseInt(year, 10);
+
+  // Check month validity
+  if (monthNumber < 1 || monthNumber > 12) return 'invalid-month';
+
+  // Try to create the date - if it fails, it's either invalid day or date rollover
+  const parsed = new Date(yearNumber, monthNumber - 1, dayNumber);
+  if (parsed.getFullYear() !== yearNumber || parsed.getMonth() !== monthNumber - 1) {
+    return 'date-rollover';
+  }
+  if (parsed.getDate() !== dayNumber) {
+    return 'invalid-day';
+  }
+  return null;
+}
+
+/**
+ * Generates a human-readable error message based on the validation issue.
+ */
+export function generateErrorMessage(reason, segments, translations) {
+  const {
+    month,
+    day
+  } = segments;
+  const monthNumber = parseInt(month, 10);
+  switch (reason) {
+    case 'incomplete':
+      return translations.invalidDateIncomplete || 'Incomplete date';
+    case 'invalid-month':
+      return translations.invalidDateInvalidMonth || 'Month must be between 1 and 12';
+    case 'invalid-day':
+      {
+        const monthName = new Date(2024, monthNumber - 1, 1).toLocaleString('en-US', {
+          month: 'long'
+        });
+        const message = translations.invalidDateInvalidDay || '{{month}} does not have {{day}} days';
+        return message.replace('{{month}}', monthName).replace('{{day}}', day);
+      }
+    case 'date-rollover':
+      {
+        const monthName = new Date(2024, monthNumber - 1, 1).toLocaleString('en-US', {
+          month: 'long'
+        });
+        const message = translations.invalidDateRollover || '{{month}} does not have {{day}} days';
+        return message.replace('{{month}}', monthName).replace('{{day}}', day);
+      }
+    case 'disabled-date':
+      return translations.invalidDateNotAvailable || 'This date is not available';
+    case 'range-contains-disabled-date':
+      return translations.invalidDateRangeContainsDisabledDate || 'This date range contains unavailable dates';
+    default:
+      return 'Invalid date';
+  }
+}
+
+/**
+ * Validates a set of date segments and returns either a valid date or an error message.
+ * Requires: year to be exactly 4 digits, month and day to have digits and parse to valid numbers.
+ * Checks for structural validity (date exists), disabled dates, and generates specific error messages.
+ */
+export function validateSegments(segments, translations, disableDate) {
+  const {
+    month,
+    day,
+    year
+  } = segments;
+
+  // Require complete year (4 digits), month, and day to have input
+  const hasCompleteYear = year.length === 4;
+  const hasMonthAndDay = month.length > 0 && day.length > 0;
+  if (!hasCompleteYear || !hasMonthAndDay) {
+    // Incomplete entry - return as valid (no error), but with no date
+    return {
+      isValid: false,
+      errorMessage: '',
+      reason: 'incomplete'
+    };
+  }
+
+  // Entry is complete (year has 4 digits, month and day have input) - validate it
+  const parsed = parseSegmentsToDate(segments);
+  if (parsed) {
+    // Date is structurally valid, now check if it's disabled
+    if (disableDate && disableDate(parsed)) {
+      return {
+        isValid: false,
+        errorMessage: generateErrorMessage('disabled-date', segments, translations),
+        reason: 'disabled-date'
+      };
+    }
+    // Date is valid and not disabled
+    return {
+      isValid: true,
+      date: parsed
+    };
+  }
+
+  // Date is not valid - detect specific reason (must be one since entry is complete)
+  const reason = detectValidationIssue(segments) || 'date-rollover';
+  const errorMessage = generateErrorMessage(reason, segments, translations);
+  return {
+    isValid: false,
+    errorMessage,
+    reason
+  };
+}
+
+/**
+ * Validates a date range to ensure no dates in the range are disabled.
+ */
+export function validateDateRange(startDate, endDate, translations, disableDate) {
+  if (!startDate || !endDate || !disableDate) return null;
+
+  // Check if any date in the range is disabled
+  const current = new Date(startDate);
+  while (current <= endDate) {
+    if (disableDate(current)) {
+      return {
+        isValid: false,
+        errorMessage: generateErrorMessage('range-contains-disabled-date', {
+          month: '',
+          day: '',
+          year: ''
+        }, translations),
+        reason: 'range-contains-disabled-date'
+      };
+    }
+    current.setDate(current.getDate() + 1);
+  }
+  return null;
+}
\ No newline at end of file