Bug 1381421 - (Part 1) Handle dates earlier than 0001-01-01 and later than 275760-09-13 correctly. r=mconley draft
authorScott Wu <scottcwwu@gmail.com>
Tue, 18 Jul 2017 10:21:25 +0800
changeset 648145 7d37ebd683f93aaf5604b43e710adad563ee8b87
parent 648046 932388b8c22c9775264e543697ce918415db9e23
child 648146 6cbea6a421030f2c50cfa8e0f736fd7a674ca56f
push id74642
push userbmo:scwwu@mozilla.com
push dateThu, 17 Aug 2017 09:24:26 +0000
reviewersmconley
bugs1381421, 275760
milestone57.0a1
Bug 1381421 - (Part 1) Handle dates earlier than 0001-01-01 and later than 275760-09-13 correctly. r=mconley MozReview-Commit-ID: Af4ZuYIxRsT
toolkit/content/widgets/calendar.js
toolkit/content/widgets/datekeeper.js
toolkit/content/widgets/datepicker.js
--- a/toolkit/content/widgets/calendar.js
+++ b/toolkit/content/widgets/calendar.js
@@ -40,17 +40,17 @@ Calendar.prototype = {
   /**
    * Set new properties and render them.
    *
    * @param {Object} props
    *        {
    *          {Boolean} isVisible: Whether or not the calendar is in view
    *          {Array<Object>} days: Data for days
    *          {
-   *            {Number} dateValue: Date in milliseconds
+   *            {Number} dateObj
    *            {Number} textContent
    *            {Array<String>} classNames
    *          }
    *          {Array<Object>} weekHeaders: Data for weekHeaders
    *          {
    *            {Number} textContent
    *            {Array<String>} classNames
    *            {Boolean} enabled
@@ -58,19 +58,19 @@ Calendar.prototype = {
    *          {Function} getDayString: Transform day number to string
    *          {Function} getWeekHeaderString: Transform day of week number to string
    *          {Function} setSelection: Set selection for dateKeeper
    *        }
    */
   setProps(props) {
     if (props.isVisible) {
       // Transform the days and weekHeaders array for rendering
-      const days = props.days.map(({ dateObj, classNames, enabled }) => {
+      const days = props.days.map(({ textContent, classNames, enabled }) => {
         return {
-          textContent: props.getDayString(dateObj.getUTCDate()),
+          textContent: props.getDayString(textContent),
           className: classNames.join(" "),
           enabled
         };
       });
       const weekHeaders = props.weekHeaders.map(({ textContent, classNames }) => {
         return {
           textContent: props.getWeekHeaderString(textContent),
           className: classNames.join(" ")
--- a/toolkit/content/widgets/datekeeper.js
+++ b/toolkit/content/widgets/datekeeper.js
@@ -14,19 +14,21 @@ function DateKeeper(props) {
 {
   const DAYS_IN_A_WEEK = 7,
         MONTHS_IN_A_YEAR = 12,
         YEAR_VIEW_SIZE = 200,
         YEAR_BUFFER_SIZE = 10,
         // The min value is 0001-01-01 based on HTML spec:
         // https://html.spec.whatwg.org/#valid-date-string
         MIN_DATE = -62135596800000,
-        // The max value is derived from the ECMAScript spec:
+        // The max value is derived from the ECMAScript spec (275760-09-13):
         // http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.1
-        MAX_DATE = 8640000000000000;
+        MAX_DATE = 8640000000000000,
+        MAX_YEAR = 275760,
+        MAX_MONTH = 9;
 
   DateKeeper.prototype = {
     get year() {
       return this.state.dateObj.getUTCFullYear();
     },
 
     get month() {
       return this.state.dateObj.getUTCMonth();
@@ -61,38 +63,46 @@ function DateKeeper(props) {
         step, firstDayOfWeek, weekends, calViewSize,
         // min & max are NaN if empty or invalid
         min: new Date(Number.isNaN(min) ? MIN_DATE : min),
         max: new Date(Number.isNaN(max) ? MAX_DATE : max),
         stepBase: new Date(stepBase),
         today: this._newUTCDate(today.getFullYear(), today.getMonth(), today.getDate()),
         weekHeaders: this._getWeekHeaders(firstDayOfWeek, weekends),
         years: [],
-        months: [],
-        days: [],
+        dateObj: new Date(0),
         selection: { year, month, day },
       };
 
-      this.state.dateObj = isDateSet ?
-                           this._newUTCDate(year, month, day) :
-                           new Date(this.state.today);
+      if (isDateSet) {
+        this.setCalendar({ year, month });
+      } else {
+        this.state.dateObj = this.state.today;
+      }
     },
+
     /**
-     * Set new date. The year is always treated as full year, so the short-form
-     * is not supported.
+     * Set new calendar date. The year is always treated as full year, so the
+     * short-form is not supported.
      * @param {Object} date parts
      *        {
      *          {Number} year [optional]
      *          {Number} month [optional]
-     *          {Number} date [optional]
      *        }
      */
-    set({ year = this.year, month = this.month, day = this.day }) {
+    setCalendar({ year = this.year, month = this.month }) {
+      // Make sure the date is valid before setting.
       // Use setUTCFullYear so that year 99 doesn't get parsed as 1999
-      this.state.dateObj.setUTCFullYear(year, month, day);
+      if (year > MAX_YEAR || year === MAX_YEAR && month >= MAX_MONTH) {
+        this.state.dateObj.setUTCFullYear(MAX_YEAR, MAX_MONTH - 1, 1);
+      } else if (year < 1 || year === 1 && month < 0) {
+        this.state.dateObj.setUTCFullYear(1, 0, 1);
+      } else {
+        this.state.dateObj.setUTCFullYear(year, month, 1);
+      }
     },
 
     /**
      * Set selection date
      * @param {Number} year
      * @param {Number} month
      * @param {Number} day
      */
@@ -102,42 +112,33 @@ function DateKeeper(props) {
       this.state.selection.day = day;
     },
 
     /**
      * Set month. Makes sure the day is <= the last day of the month
      * @param {Number} month
      */
     setMonth(month) {
-      const lastDayOfMonth = this._newUTCDate(this.year, month + 1, 0).getUTCDate();
-      this.set({ year: this.year,
-                 month,
-                 day: Math.min(this.day, lastDayOfMonth) });
+      this.setCalendar({ year: this.year, month });
     },
 
     /**
      * Set year. Makes sure the day is <= the last day of the month
      * @param {Number} year
      */
     setYear(year) {
-      const lastDayOfMonth = this._newUTCDate(year, this.month + 1, 0).getUTCDate();
-      this.set({ year,
-                 month: this.month,
-                 day: Math.min(this.day, lastDayOfMonth) });
+      this.setCalendar({ year, month: this.month });
     },
 
     /**
      * Set month by offset. Makes sure the day is <= the last day of the month
      * @param {Number} offset
      */
     setMonthByOffset(offset) {
-      const lastDayOfMonth = this._newUTCDate(this.year, this.month + offset + 1, 0).getUTCDate();
-      this.set({ year: this.year,
-                 month: this.month + offset,
-                 day: Math.min(this.day, lastDayOfMonth) });
+      this.setCalendar({ year: this.year, month: this.month + offset });
     },
 
     /**
      * Generate the array of months
      * @return {Array<Object>}
      *         {
      *           {Number} value: Month in int
      *           {Boolean} enabled
@@ -173,20 +174,23 @@ function DateKeeper(props) {
 
       // Generate new years array when the year is outside of the first &
       // last item range. If not, return the cached result.
       if (!firstItem || !lastItem ||
           currentYear <= firstItem.value + YEAR_BUFFER_SIZE ||
           currentYear >= lastItem.value - YEAR_BUFFER_SIZE) {
         // The year is set in the middle with items on both directions
         for (let i = -(YEAR_VIEW_SIZE / 2); i < YEAR_VIEW_SIZE / 2; i++) {
-          years.push({
-            value: currentYear + i,
-            enabled: true
-          });
+          const year = currentYear + i;
+          if (year >= 1 && year <= MAX_YEAR) {
+            years.push({
+              value: year,
+              enabled: true
+            });
+          }
         }
         this.state.years = years;
       }
       return this.state.years;
     },
 
     /**
      * Get days for calendar
@@ -201,19 +205,32 @@ function DateKeeper(props) {
       const firstDayOfMonth = this._getFirstCalendarDate(this.state.dateObj, this.state.firstDayOfWeek);
       const month = this.month;
       let days = [];
 
       for (let i = 0; i < this.state.calViewSize; i++) {
         const dateObj = this._newUTCDate(firstDayOfMonth.getUTCFullYear(),
                                          firstDayOfMonth.getUTCMonth(),
                                          firstDayOfMonth.getUTCDate() + i);
+
         let classNames = [];
         let enabled = true;
 
+        const isValid = dateObj.getTime() >= MIN_DATE && dateObj.getTime() <= MAX_DATE;
+        if (!isValid) {
+          classNames.push("out-of-range");
+          enabled = false;
+
+          days.push({
+            classNames,
+            enabled,
+          });
+          continue;
+        }
+
         const isWeekend = this.state.weekends.includes(dateObj.getUTCDay());
         const isCurrentMonth = month == dateObj.getUTCMonth();
         const isSelection = this.state.selection.year == dateObj.getUTCFullYear() &&
                             this.state.selection.month == dateObj.getUTCMonth() &&
                             this.state.selection.day == dateObj.getUTCDate();
         const isOutOfRange = dateObj.getTime() < this.state.min.getTime() ||
                              dateObj.getTime() > this.state.max.getTime();
         const isToday = this.state.today.getTime() == dateObj.getTime();
@@ -239,16 +256,17 @@ function DateKeeper(props) {
           classNames.push("today");
         }
         if (isOffStep) {
           classNames.push("off-step");
           enabled = false;
         }
         days.push({
           dateObj,
+          textContent: dateObj.getUTCDate(),
           classNames,
           enabled,
         });
       }
       return days;
     },
 
     /**
@@ -313,12 +331,12 @@ function DateKeeper(props) {
     },
 
     /**
      * Helper function for creating UTC dates
      * @param  {...[Number]} parts
      * @return {Date}
      */
     _newUTCDate(...parts) {
-      return new Date(Date.UTC(...parts));
-    }
+      return new Date(new Date(0).setUTCFullYear(...parts));
+    },
   };
 }
--- a/toolkit/content/widgets/datepicker.js
+++ b/toolkit/content/widgets/datepicker.js
@@ -57,17 +57,17 @@ function DatePicker(context) {
       document.dir = dir;
 
       this.state = {
         dateKeeper,
         locale,
         isMonthPickerVisible: false,
         datetimeOrders: new Intl.DateTimeFormat(locale)
                           .formatToParts(new Date(0)).map(part => part.type),
-        getDayString: new Intl.NumberFormat(locale).format,
+        getDayString: day => day ? new Intl.NumberFormat(locale).format(day) : "",
         getWeekHeaderString: weekday => weekdayStrings[weekday],
         getMonthString: month => monthStrings[month],
         setSelection: date => {
           dateKeeper.setSelection({
             year: date.getUTCFullYear(),
             month: date.getUTCMonth(),
             day: date.getUTCDate(),
           });
@@ -258,18 +258,18 @@ function DatePicker(context) {
      *          {Number} year [optional]
      *          {Number} month [optional]
      *          {Number} date [optional]
      *        }
      */
     set({ year, month, day }) {
       const { dateKeeper } = this.state;
 
-      dateKeeper.set({
-        year, month, day
+      dateKeeper.setCalendar({
+        year, month
       });
       dateKeeper.setSelection({
         year, month, day
       });
       this._update();
     }
   };