diff options
author | Alon Zakai <alonzakai@gmail.com> | 2013-07-15 10:16:28 -0700 |
---|---|---|
committer | Alon Zakai <alonzakai@gmail.com> | 2013-07-15 10:16:28 -0700 |
commit | b52b41fa588236ead80a2bffbfb88d5d8c8d225e (patch) | |
tree | 4bc9a68849550742f82b3abfe563c3358d0b7d4c | |
parent | b87be16d64b81efd5c5e9187d3cd601441b4af31 (diff) | |
parent | c2a470bbdb65b5e433fe8c123afc38fc4b7a5dcc (diff) |
Merge pull request #1383 from sbalko/incoming
Implement strptime
-rw-r--r-- | AUTHORS | 1 | ||||
-rw-r--r-- | src/library.js | 274 | ||||
-rwxr-xr-x | tests/runner.py | 124 |
3 files changed, 398 insertions, 1 deletions
@@ -87,3 +87,4 @@ a license to everyone to use it as detailed in LICENSE.) * Manfred Manik Nerurkar <nerurkar*at*made-apps.biz> (copyright owned by MADE, GmbH) * Joseph Gentle <me@josephg.com> * Douglas T. Crosher <dtc-moz@scieneer.com> (copyright owned by Mozilla Founcation) +* Soeren Balko <soeren.balko@gmail.com> diff --git a/src/library.js b/src/library.js index 20a163c6..24e35178 100644 --- a/src/library.js +++ b/src/library.js @@ -6215,10 +6215,282 @@ LibraryManager.library = { }, strftime_l: 'strftime', // no locale support yet + strptime__deps: ['__tm_struct_layout'], strptime: function(buf, format, tm) { // char *strptime(const char *restrict buf, const char *restrict format, struct tm *restrict tm); // http://pubs.opengroup.org/onlinepubs/009695399/functions/strptime.html - // TODO: Implement. + var pattern = Pointer_stringify(format); + + // escape special characters + // TODO: not sure we really need to escape all of these in JS regexps + var SPECIAL_CHARS = '\\!@#$^&*()+=-[]/{}|:<>?,.'; + for (var i=0, ii=SPECIAL_CHARS.length; i<ii; ++i) { + pattern = pattern.replace(new RegExp('\\'+SPECIAL_CHARS[i], 'g'), '\\'+SPECIAL_CHARS[i]); + } + + // reduce number of matchers + var EQUIVALENT_MATCHERS = { + '%A': '%a', + '%B': '%b', + '%c': '%x\\s+%X', + '%D': '%m\\/%d\\/%y', + '%e': '%d', + '%h': '%b', + '%R': '%H\\:%M', + '%r': '%I\\:%M\\:%S\\s%p', + '%T': '%H\\:%M\\:%S', + '%x': '%m\\/%d\\/(?:%y|%Y)', + '%X': '%H\\:%M\\:%S' + }; + for (var matcher in EQUIVALENT_MATCHERS) { + pattern = pattern.replace(matcher, EQUIVALENT_MATCHERS[matcher]); + } + + // TODO: take care of locale + + var DATE_PATTERNS = { + /* weeday name */ '%a': '(?:Sun(?:day)?)|(?:Mon(?:day)?)|(?:Tue(?:sday)?)|(?:Wed(?:nesday)?)|(?:Thu(?:rsday)?)|(?:Fri(?:day)?)|(?:Sat(?:urday)?)', + /* month name */ '%b': '(?:Jan(?:uary)?)|(?:Feb(?:ruary)?)|(?:Mar(?:ch)?)|(?:Apr(?:il)?)|May|(?:Jun(?:e)?)|(?:Jul(?:y)?)|(?:Aug(?:ust)?)|(?:Sep(?:tember)?)|(?:Oct(?:ober)?)|(?:Nov(?:ember)?)|(?:Dec(?:ember)?)', + /* century */ '%C': '\\d\\d', + /* day of month */ '%d': '0[1-9]|[1-9](?!\\d)|1\\d|2\\d|30|31', + /* hour (24hr) */ '%H': '\\d(?!\\d)|[0,1]\\d|20|21|22|23', + /* hour (12hr) */ '%I': '\\d(?!\\d)|0\\d|10|11|12', + /* day of year */ '%j': '00[1-9]|0?[1-9](?!\\d)|0?[1-9]\\d(?!\\d)|[1,2]\\d\\d|3[0-6]\\d', + /* month */ '%m': '0[1-9]|[1-9](?!\\d)|10|11|12', + /* minutes */ '%M': '0\\d|\\d(?!\\d)|[1-5]\\d', + /* whitespace */ '%n': '\\s', + /* AM/PM */ '%p': 'AM|am|PM|pm|A\\.M\\.|a\\.m\\.|P\\.M\\.|p\\.m\\.', + /* seconds */ '%S': '0\\d|\\d(?!\\d)|[1-5]\\d|60', + /* week number */ '%U': '0\\d|\\d(?!\\d)|[1-4]\\d|50|51|52|53', + /* week number */ '%W': '0\\d|\\d(?!\\d)|[1-4]\\d|50|51|52|53', + /* weekday number */ '%w': '[0-6]', + /* 2-digit year */ '%y': '\\d\\d', + /* 4-digit year */ '%Y': '\\d\\d\\d\\d', + /* % */ '%%': '%', + /* whitespace */ '%t': '\\s', + }; + + var MONTH_NUMBERS = {JAN: 0, FEB: 1, MAR: 2, APR: 3, MAY: 4, JUN: 5, JUL: 6, AUG: 7, SEP: 8, OCT: 9, NOV: 10, DEC: 11}; + var MONTH_DAYS_REGULAR = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + var MONTH_DAYS_LEAP = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + var DAY_NUMBERS_SUN_FIRST = {SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6}; + var DAY_NUMBERS_MON_FIRST = {MON: 0, TUE: 1, WED: 2, THU: 3, FRI: 4, SAT: 5, SUN: 6}; + + var isLeapYear = function(year) { + return year%4===0 && (year%100!==0 || year%400===0); + }; + + var arraySum = function(array, index) { + var sum = 0; + for (var i=0; i<=index; sum += array[i++]); + return sum; + }; + + var addDays = function(date, days) { + while(days>0) { + var leap = isLeapYear(date.getFullYear()); + var currentMonth = date.getMonth(); + var daysInCurrentMonth = (leap ? MONTH_DAYS_LEAP : MONTH_DAYS_REGULAR)[currentMonth]; + + if (days>daysInCurrentMonth-date.getDate()) { + // we spill over to next month + days -= (daysInCurrentMonth-date.getDate()+1); + date.setDate(1); + if (currentMonth<11) { + date.setMonth(currentMonth+1) + } else { + date.setMonth(0); + date.setFullYear(date.getFullYear()+1); + } + } else { + // we stay in current month + date.setDate(date.getDate()+days); + return date; + } + } + return date; + }; + + for (var datePattern in DATE_PATTERNS) { + pattern = pattern.replace(datePattern, '('+datePattern+DATE_PATTERNS[datePattern]+')'); + } + + // take care of capturing groups + var capture = []; + for (var i=pattern.indexOf('%'); i>=0; i=pattern.indexOf('%')) { + capture.push(pattern[i+1]); + pattern = pattern.replace(new RegExp('\\%'+pattern[i+1], 'g'), ''); + } + + var matches = new RegExp('^'+pattern).exec(Pointer_stringify(buf)) + // Module['print'](Pointer_stringify(buf)+ ' is matched by '+((new RegExp('^'+pattern)).source)+' into: '+JSON.stringify(matches)); + + var initDate = function() { + var fixup = function(value, min, max) { + return (typeof value !== 'number' || isNaN(value)) ? min : (value>=min ? (value<=max ? value: max): min); + }; + + return { + year: fixup({{{ makeGetValue('tm', '___tm_struct_layout.tm_year', 'i32') }}} + 1900 , 1970, 9999), + month: fixup({{{ makeGetValue('tm', '___tm_struct_layout.tm_mon', 'i32') }}}, 0, 11), + day: fixup({{{ makeGetValue('tm', '___tm_struct_layout.tm_mday', 'i32') }}}, 1, 31), + hour: fixup({{{ makeGetValue('tm', '___tm_struct_layout.tm_hour', 'i32') }}}, 0, 23), + min: fixup({{{ makeGetValue('tm', '___tm_struct_layout.tm_min', 'i32') }}}, 0, 59), + sec: fixup({{{ makeGetValue('tm', '___tm_struct_layout.tm_sec', 'i32') }}}, 0, 59) + }; + }; + + if (matches) { + var date = initDate(); + var value; + + var getMatch = function(symbol) { + var pos = capture.indexOf(symbol); + // check if symbol appears in regexp + if (pos >= 0) { + // return matched value or null (falsy!) for non-matches + return matches[pos+1]; + } + return; + } + + // seconds + if ((value=getMatch('S'))) { + date.sec = parseInt(value); + } + + // minutes + if ((value=getMatch('M'))) { + date.min = parseInt(value); + } + + // hours + if ((value=getMatch('H'))) { + // 24h clock + date.hour = parseInt(value); + } else if ((value = getMatch('I'))) { + // AM/PM clock + var hour = parseInt(value); + if ((value=getMatch('p'))) { + hour += value.toUpperCase()[0] === 'P' ? 12 : 0; + } + date.hour = hour; + } + + // year + if ((value=getMatch('Y'))) { + // parse from four-digit year + date.year = parseInt(value); + } else if ((value=getMatch('y'))) { + // parse from two-digit year... + var year = parseInt(value); + if ((value=getMatch('C'))) { + // ...and century + year += parseInt(value)*100; + } else { + // ...and rule-of-thumb + year += year<69 ? 2000 : 1900; + } + date.year = year; + } + + // month + if ((value=getMatch('m'))) { + // parse from month number + date.month = parseInt(value)-1; + } else if ((value=getMatch('b'))) { + // parse from month name + date.month = MONTH_NUMBERS[value.substring(0,3).toUpperCase()] || 0; + // TODO: derive month from day in year+year, week number+day of week+year + } + + // day + if ((value=getMatch('d'))) { + // get day of month directly + date.day = parseInt(value); + } else if ((value=getMatch('j'))) { + // get day of month from day of year ... + var day = parseInt(value); + var leapYear = isLeapYear(date.year); + for (var month=0; month<12; ++month) { + var daysUntilMonth = arraySum(leapYear ? MONTH_DAYS_LEAP : MONTH_DAYS_REGULAR, month-1); + if (day<=daysUntilMonth+(leapYear ? MONTH_DAYS_LEAP : MONTH_DAYS_REGULAR)[month]) { + date.day = day-daysUntilMonth; + } + } + } else if ((value=getMatch('a'))) { + // get day of month from weekday ... + var weekDay = value.substring(0,3).toUpperCase(); + if ((value=getMatch('U'))) { + // ... and week number (Sunday being first day of week) + // Week number of the year (Sunday as the first day of the week) as a decimal number [00,53]. + // All days in a new year preceding the first Sunday are considered to be in week 0. + var weekDayNumber = DAY_NUMBERS_SUN_FIRST[weekDay]; + var weekNumber = parseInt(value); + + // January 1st + var janFirst = new Date(date.year, 0, 1); + var endDate; + if (janFirst.getDay() === 0) { + // Jan 1st is a Sunday, and, hence in the 1st CW + endDate = addDays(janFirst, weekDayNumber+7*(weekNumber-1)); + } else { + // Jan 1st is not a Sunday, and, hence still in the 0th CW + endDate = addDays(janFirst, 7-janFirst.getDay()+weekDayNumber+7*(weekNumber-1)); + } + date.day = endDate.getDate(); + date.month = endDate.getMonth(); + } else if ((value=getMatch('W'))) { + // ... and week number (Monday being first day of week) + // Week number of the year (Monday as the first day of the week) as a decimal number [00,53]. + // All days in a new year preceding the first Monday are considered to be in week 0. + var weekDayNumber = DAY_NUMBERS_MON_FIRST[weekDay]; + var weekNumber = parseInt(value); + + // January 1st + var janFirst = new Date(date.year, 0, 1); + var endDate; + if (janFirst.getDay()===1) { + // Jan 1st is a Monday, and, hence in the 1st CW + endDate = addDays(janFirst, weekDayNumber+7*(weekNumber-1)); + } else { + // Jan 1st is not a Monday, and, hence still in the 0th CW + endDate = addDays(janFirst, 7-janFirst.getDay()+1+weekDayNumber+7*(weekNumber-1)); + } + + date.day = endDate.getDate(); + date.month = endDate.getMonth(); + } + } + + /* + tm_sec int seconds after the minute 0-61* + tm_min int minutes after the hour 0-59 + tm_hour int hours since midnight 0-23 + tm_mday int day of the month 1-31 + tm_mon int months since January 0-11 + tm_year int years since 1900 + tm_wday int days since Sunday 0-6 + tm_yday int days since January 1 0-365 + tm_isdst int Daylight Saving Time flag + */ + + var fullDate = new Date(date.year, date.month, date.day, date.hour, date.min, date.sec, 0); + {{{ makeSetValue('tm', '___tm_struct_layout.tm_sec', 'fullDate.getSeconds()', 'i32') }}} + {{{ makeSetValue('tm', '___tm_struct_layout.tm_min', 'fullDate.getMinutes()', 'i32') }}} + {{{ makeSetValue('tm', '___tm_struct_layout.tm_hour', 'fullDate.getHours()', 'i32') }}} + {{{ makeSetValue('tm', '___tm_struct_layout.tm_mday', 'fullDate.getDate()', 'i32') }}} + {{{ makeSetValue('tm', '___tm_struct_layout.tm_mon', 'fullDate.getMonth()', 'i32') }}} + {{{ makeSetValue('tm', '___tm_struct_layout.tm_year', 'fullDate.getFullYear()-1900', 'i32') }}} + {{{ makeSetValue('tm', '___tm_struct_layout.tm_wday', 'fullDate.getDay()', 'i32') }}} + {{{ makeSetValue('tm', '___tm_struct_layout.tm_yday', 'arraySum(isLeapYear(fullDate.getFullYear()) ? MONTH_DAYS_LEAP : MONTH_DAYS_REGULAR, fullDate.getMonth()-1)+fullDate.getDate()-1', 'i32') }}} + {{{ makeSetValue('tm', '___tm_struct_layout.tm_isdst', '0', 'i32') }}} + + // we need to convert the matched sequence into an integer array to take care of UTF-8 characters > 0x7F + // TODO: not sure that intArrayFromString handles all unicode characters correctly + return buf+intArrayFromString(matches[0]).length-1; + } + return 0; }, strptime_l: 'strptime', // no locale support yet diff --git a/tests/runner.py b/tests/runner.py index ea702689..1f6b39f4 100755 --- a/tests/runner.py +++ b/tests/runner.py @@ -4958,6 +4958,130 @@ The current type of b is: 9 ''' self.do_run(src, 'time: ') # compilation check, mainly + + def test_strptime_tm(self): + src=r''' + #include <time.h> + #include <stdio.h> + #include <string.h> + + int main() { + struct tm tm; + char *ptr = strptime("17410105012000", "%H%M%S%d%m%Y", &tm); + + printf("%s: %s, %d/%d/%d %d:%d:%d", + (ptr != NULL && *ptr=='\0') ? "OK" : "ERR", + tm.tm_wday == 0 ? "Sun" : (tm.tm_wday == 1 ? "Mon" : (tm.tm_wday == 2 ? "Tue" : (tm.tm_wday == 3 ? "Wed" : (tm.tm_wday == 4 ? "Thu" : (tm.tm_wday == 5 ? "Fri" : (tm.tm_wday == 6 ? "Sat" : "ERR")))))), + tm.tm_mon+1, + tm.tm_mday, + tm.tm_year+1900, + tm.tm_hour, + tm.tm_min, + tm.tm_sec + ); + } + ''' + self.do_run(src, 'OK: Wed, 1/5/2000 17:41:1') + + def test_strptime_days(self): + src = r''' + #include <time.h> + #include <stdio.h> + #include <string.h> + + static const struct { + const char *input; + const char *format; + } day_tests[] = { + { "2000-01-01", "%Y-%m-%d"}, + { "03/03/00", "%D"}, + { "9/9/99", "%x"}, + { "19990502123412", "%Y%m%d%H%M%S"}, + { "2001 20 Mon", "%Y %U %a"}, + { "2006 4 Fri", "%Y %U %a"}, + { "2001 21 Mon", "%Y %W %a"}, + { "2013 29 Wed", "%Y %W %a"}, + { "2000-01-01 08:12:21 AM", "%Y-%m-%d %I:%M:%S %p"}, + { "2000-01-01 08:12:21 PM", "%Y-%m-%d %I:%M:%S %p"}, + { "2001 17 Tue", "%Y %U %a"}, + { "2001 8 Thursday", "%Y %W %a"}, + }; + + int main() { + struct tm tm; + + for (int i = 0; i < sizeof (day_tests) / sizeof (day_tests[0]); ++i) { + memset (&tm, '\0', sizeof (tm)); + char *ptr = strptime(day_tests[i].input, day_tests[i].format, &tm); + + printf("%s: %d/%d/%d (%dth DoW, %dth DoY)\n", (ptr != NULL && *ptr=='\0') ? "OK" : "ERR", tm.tm_mon+1, tm.tm_mday, 1900+tm.tm_year, tm.tm_wday, tm.tm_yday); + } + } + ''' + self.do_run(src, 'OK: 1/1/2000 (6th DoW, 0th DoY)\n'\ + 'OK: 3/3/2000 (5th DoW, 62th DoY)\n'\ + 'OK: 9/9/1999 (4th DoW, 251th DoY)\n'\ + 'OK: 5/2/1999 (0th DoW, 121th DoY)\n'\ + 'OK: 5/21/2001 (1th DoW, 140th DoY)\n'\ + 'OK: 1/27/2006 (5th DoW, 26th DoY)\n'\ + 'OK: 5/21/2001 (1th DoW, 140th DoY)\n'\ + 'OK: 7/24/2013 (3th DoW, 204th DoY)\n'\ + 'OK: 1/1/2000 (6th DoW, 0th DoY)\n'\ + 'OK: 1/1/2000 (6th DoW, 0th DoY)\n'\ + 'OK: 5/1/2001 (2th DoW, 120th DoY)\n'\ + 'OK: 2/22/2001 (4th DoW, 52th DoY)\n'\ + ) + + def test_strptime_reentrant(self): + src=r''' + #include <time.h> + #include <stdio.h> + #include <string.h> + #include <stdlib.h> + + int main () { + int result = 0; + struct tm tm; + + memset (&tm, 0xaa, sizeof (tm)); + + /* Test we don't crash on uninitialized struct tm. + Some fields might contain bogus values until everything + needed is initialized, but we shouldn't crash. */ + if (strptime ("2007", "%Y", &tm) == NULL + || strptime ("12", "%d", &tm) == NULL + || strptime ("Feb", "%b", &tm) == NULL + || strptime ("13", "%M", &tm) == NULL + || strptime ("21", "%S", &tm) == NULL + || strptime ("16", "%H", &tm) == NULL) { + printf("ERR: returned NULL"); + exit(EXIT_FAILURE); + } + + if (tm.tm_sec != 21 || tm.tm_min != 13 || tm.tm_hour != 16 + || tm.tm_mday != 12 || tm.tm_mon != 1 || tm.tm_year != 107 + || tm.tm_wday != 1 || tm.tm_yday != 42) { + printf("ERR: unexpected tm content (1) - %d/%d/%d %d:%d:%d", tm.tm_mon+1, tm.tm_mday, tm.tm_year+1900, tm.tm_hour, tm.tm_min, tm.tm_sec); + exit(EXIT_FAILURE); + } + + if (strptime ("8", "%d", &tm) == NULL) { + printf("ERR: strptime failed"); + exit(EXIT_FAILURE); + } + + if (tm.tm_sec != 21 || tm.tm_min != 13 || tm.tm_hour != 16 + || tm.tm_mday != 8 || tm.tm_mon != 1 || tm.tm_year != 107 + || tm.tm_wday != 4 || tm.tm_yday != 38) { + printf("ERR: unexpected tm content (2) - %d/%d/%d %d:%d:%d", tm.tm_mon+1, tm.tm_mday, tm.tm_year+1900, tm.tm_hour, tm.tm_min, tm.tm_sec); + exit(EXIT_FAILURE); + } + + printf("OK"); + } + ''' + self.do_run(src, 'OK') + def test_intentional_fault(self): # Some programs intentionally segfault themselves, we should compile that into a throw src = r''' |