Browse Source

Merge branch 'gmtime-refactor' into 'master'

Refactor gmtime_r(), fix localtime() test

See merge request redox-os/relibc!309
Jeremy Soller 4 years ago
parent
commit
2ed55a926b
2 changed files with 99 additions and 76 deletions
  1. 95 72
      src/header/time/mod.rs
  2. 4 4
      tests/expected/time/localtime.stdout

+ 95 - 72
src/header/time/mod.rs

@@ -1,7 +1,9 @@
 //! time implementation for Redox, following http://pubs.opengroup.org/onlinepubs/7908799/xsh/time.h.html
 
+use core::convert::{TryFrom, TryInto};
+
 use crate::{
-    header::errno::EIO,
+    header::errno::{EIO, EOVERFLOW},
     platform::{self, types::*, Pal, Sys},
 };
 
@@ -187,82 +189,103 @@ fn leap_year(year: c_int) -> bool {
 }
 
 #[no_mangle]
-pub unsafe extern "C" fn gmtime_r(clock: *const time_t, t: *mut tm) -> *mut tm {
-    let clock = *clock;
-
-    let mut day = (clock / (60 * 60 * 24)) as c_int;
-    if clock < 0 && clock % (60 * 60 * 24) != 0 {
-        // -1 because for negative values round upwards
-        // -0.3 == 0, but we want -1
-        day -= 1;
-    }
-
-    (*t).tm_sec = (clock % 60) as c_int;
-    (*t).tm_min = ((clock / 60) % 60) as c_int;
-    (*t).tm_hour = ((clock / (60 * 60)) % 24) as c_int;
-
-    while (*t).tm_sec < 0 {
-        (*t).tm_sec += 60;
-        (*t).tm_min -= 1;
-    }
-    while (*t).tm_min < 0 {
-        (*t).tm_min += 60;
-        (*t).tm_hour -= 1;
-    }
-    while (*t).tm_hour < 0 {
-        (*t).tm_hour += 24;
-    }
-
-    // Jan 1th was a thursday, 4th of a zero-indexed week.
-    (*t).tm_wday = (day + 4) % 7;
-    if (*t).tm_wday < 0 {
-        (*t).tm_wday += 7;
-    }
-
-    let mut year = 1970;
-    if day < 0 {
-        while day < 0 {
-            let days_in_year = if leap_year(year) { 366 } else { 365 };
-
-            day += days_in_year;
-            year -= 1;
-        }
-        (*t).tm_year = year - 1900;
-        (*t).tm_yday = day + 1;
+pub unsafe extern "C" fn gmtime_r(clock: *const time_t, result: *mut tm) -> *mut tm {
+    /* For the details of the algorithm used here, see
+     * http://howardhinnant.github.io/date_algorithms.html#civil_from_days
+     * Note that we need 0-based months here, though.
+     * Overall, this implementation should generate correct results as
+     * long as the tm_year value will fit in a c_int. */
+    const SECS_PER_DAY: time_t = 24 * 60 * 60;
+    const DAYS_PER_ERA: time_t = 146097;
+
+    let unix_secs = *clock;
+
+    /* Day number here is possibly negative, remainder will always be
+     * nonnegative when using Euclidean division */
+    let unix_days: time_t = unix_secs.div_euclid(SECS_PER_DAY);
+
+    /* In range [0, 86399]. Needs a u32 since this is larger (at least
+     * theoretically) than the guaranteed range of c_int */
+    let secs_of_day: u32 = unix_secs.rem_euclid(SECS_PER_DAY).try_into().unwrap();
+
+    /* Shift origin from 1970-01-01 to 0000-03-01 and find out where we
+     * are in terms of 400-year eras since then */
+    let days_since_origin = unix_days + 719468;
+    let era = days_since_origin.div_euclid(DAYS_PER_ERA);
+    let day_of_era = days_since_origin.rem_euclid(DAYS_PER_ERA);
+    let year_of_era =
+        (day_of_era - day_of_era / 1460 + day_of_era / 36524 - day_of_era / 146096) / 365;
+
+    /* "transformed" here refers to dates in a calendar where years
+     * start on March 1 */
+    let year_transformed = year_of_era + 400 * era; // retain large range, don't convert to c_int yet
+    let day_of_year_transformed: c_int = (day_of_era
+        - (365 * year_of_era + year_of_era / 4 - year_of_era / 100))
+        .try_into()
+        .unwrap();
+    let month_transformed: c_int = (5 * day_of_year_transformed + 2) / 153;
+
+    // Convert back to calendar with year starting on January 1
+    let month: c_int = (month_transformed + 2) % 12; // adapted to 0-based months
+    let year: time_t = if month < 2 {
+        year_transformed + 1
     } else {
-        loop {
-            let days_in_year = if leap_year(year) { 366 } else { 365 };
-
-            if day < days_in_year {
-                break;
-            }
+        year_transformed
+    };
 
-            day -= days_in_year;
-            year += 1;
+    /* Subtract 1900 *before* converting down to c_int in order to
+     * maximize the range of input timestamps that will succeed */
+    match c_int::try_from(year - 1900) {
+        Ok(year_less_1900) => {
+            let mday: c_int = (day_of_year_transformed - (153 * month_transformed + 2) / 5 + 1)
+                .try_into()
+                .unwrap();
+
+            /* 1970-01-01 was a Thursday. Again, Euclidean division is
+             * used to ensure a nonnegative remainder (range [0, 6]). */
+            let wday: c_int = ((unix_days + 4).rem_euclid(7)).try_into().unwrap();
+
+            /* Yes, duplicated code for now (to work on non-c_int-values
+             * so that we are not constrained by the subtraction of
+             * 1900) */
+            let is_leap_year: bool = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
+
+            /* For dates that are March 1 or later, we can use day-of-
+             * year in the transformed calendar. For January and
+             * February, that value is sensitive to whether the previous
+             * year is a leap year. Therefore, we use the already
+             * computed date for those two months. */
+            let yday: c_int = match month {
+                0 => mday - 1,      // January
+                1 => 31 + mday - 1, // February
+                _ => day_of_year_transformed + if is_leap_year { 60 } else { 59 },
+            };
+
+            let hour: c_int = (secs_of_day / (60 * 60)).try_into().unwrap();
+            let min: c_int = ((secs_of_day / 60) % 60).try_into().unwrap();
+            let sec: c_int = (secs_of_day % 60).try_into().unwrap();
+
+            *result = tm {
+                tm_sec: sec,
+                tm_min: min,
+                tm_hour: hour,
+                tm_mday: mday,
+                tm_mon: month,
+                tm_year: year_less_1900,
+                tm_wday: wday,
+                tm_yday: yday,
+                tm_isdst: 0,
+                tm_gmtoff: 0,
+                tm_zone: UTC,
+            };
+
+            result
         }
-        (*t).tm_year = year - 1900;
-        (*t).tm_yday = day;
-    }
-
-    let leap = if leap_year(year) { 1 } else { 0 };
-    (*t).tm_mon = 0;
-    loop {
-        let days_in_month = MONTH_DAYS[leap][(*t).tm_mon as usize];
-
-        if day < days_in_month {
-            break;
+        Err(_) => {
+            platform::errno = EOVERFLOW;
+            core::ptr::null_mut()
         }
-
-        day -= days_in_month;
-        (*t).tm_mon += 1;
     }
-    (*t).tm_mday = 1 + day as c_int;
-
-    (*t).tm_isdst = 0;
-    (*t).tm_gmtoff = 0;
-    (*t).tm_zone = UTC;
-
-    t
 }
 
 #[no_mangle]

+ 4 - 4
tests/expected/time/localtime.stdout

@@ -1,7 +1,7 @@
-Year 69, Day of year: 333, Month 10, Day of month: 29, Day of week: 6, 0:0:0
-Year 69, Day of year: 365, Month 11, Day of month: 31, Day of week: 3, 0:0:0
-Year 69, Day of year: 365, Month 11, Day of month: 31, Day of week: 3, 23:59:59
-Year 69, Day of year: 365, Month 11, Day of month: 31, Day of week: 3, 23:51:40
+Year 69, Day of year: 332, Month 10, Day of month: 29, Day of week: 6, 0:0:0
+Year 69, Day of year: 364, Month 11, Day of month: 31, Day of week: 3, 0:0:0
+Year 69, Day of year: 364, Month 11, Day of month: 31, Day of week: 3, 23:59:59
+Year 69, Day of year: 364, Month 11, Day of month: 31, Day of week: 3, 23:51:40
 Year 70, Day of year: 0, Month 0, Day of month: 1, Day of week: 4, 0:0:0
 Year 70, Day of year: 0, Month 0, Day of month: 1, Day of week: 4, 0:0:1
 Year 118, Day of year: 193, Month 6, Day of month: 13, Day of week: 5, 4:9:10