Browse Source

Refactor gmtime_r()

Peter Limkilde Svendsen 4 years ago
parent
commit
845c74a82e
1 changed files with 86 additions and 73 deletions
  1. 86 73
      src/header/time/mod.rs

+ 86 - 73
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},
 };
 
@@ -188,81 +190,92 @@ fn leap_year(year: c_int) -> bool {
 
 #[no_mangle]
 pub unsafe extern "C" fn gmtime_r(clock: *const time_t, result: *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;
-    }
-
-    (*result).tm_sec = (clock % 60) as c_int;
-    (*result).tm_min = ((clock / 60) % 60) as c_int;
-    (*result).tm_hour = ((clock / (60 * 60)) % 24) as c_int;
-
-    while (*result).tm_sec < 0 {
-        (*result).tm_sec += 60;
-        (*result).tm_min -= 1;
-    }
-    while (*result).tm_min < 0 {
-        (*result).tm_min += 60;
-        (*result).tm_hour -= 1;
-    }
-    while (*result).tm_hour < 0 {
-        (*result).tm_hour += 24;
-    }
-
-    // Jan 1th was a thursday, 4th of a zero-indexed week.
-    (*result).tm_wday = (day + 4) % 7;
-    if (*result).tm_wday < 0 {
-        (*result).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;
+    /* 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 {year_transformed};
+    
+    /* 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
         }
-        (*result).tm_year = year - 1900;
-        (*result).tm_yday = day + 1;
-    } else {
-        loop {
-            let days_in_year = if leap_year(year) { 366 } else { 365 };
-
-            if day < days_in_year {
-                break;
-            }
-
-            day -= days_in_year;
-            year += 1;
-        }
-        (*result).tm_year = year - 1900;
-        (*result).tm_yday = day;
-    }
-
-    let leap = if leap_year(year) { 1 } else { 0 };
-    (*result).tm_mon = 0;
-    loop {
-        let days_in_month = MONTH_DAYS[leap][(*result).tm_mon as usize];
-
-        if day < days_in_month {
-            break;
+        Err(_) => {
+            platform::errno = EOVERFLOW;
+            core::ptr::null_mut()
         }
-
-        day -= days_in_month;
-        (*result).tm_mon += 1;
     }
-    (*result).tm_mday = 1 + day as c_int;
-
-    (*result).tm_isdst = 0;
-    (*result).tm_gmtoff = 0;
-    (*result).tm_zone = UTC;
-
-    result
 }
 
 #[no_mangle]