Skip to content

Use musl's strptime implementation #22158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ See docs/process.md for more on how version tagging works.

3.1.63 (in development)
-----------------------
- Emscripten now uses `strptime` from musl rather than using a custom
JavaScript implementation. (#21379)

3.1.62 - 07/02/24
-----------------
Expand Down
264 changes: 0 additions & 264 deletions src/library.js
Original file line number Diff line number Diff line change
Expand Up @@ -728,270 +728,6 @@ addToLibrary({
return newDate;
},

strptime__deps: ['$isLeapYear', '$arraySum', '$addDays', '$MONTH_DAYS_REGULAR', '$MONTH_DAYS_LEAP',
'$jstoi_q', '$intArrayFromString' ],
strptime: (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
var pattern = UTF8ToString(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': '%a %b %d %H:%M:%S %Y',
'D': '%m\\/%d\\/%y',
'e': '%d',
'F': '%Y-%m-%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'
};
// TODO: take care of locale

var DATE_PATTERNS = {
/* weekday 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': ' ',
/* 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': ' ',
/* time zone */ 'z': 'Z|(?:[\\+\\-]\\d\\d:?(?:\\d\\d)?)'
};

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 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 capture = [];
var pattern_out = pattern
.replace(/%(.)/g, (m, c) => EQUIVALENT_MATCHERS[c] || m)
.replace(/%(.)/g, (_, c) => {
let pat = DATE_PATTERNS[c];
if (pat){
capture.push(c);
return `(${pat})`;
} else {
return c;
}
})
.replace( // any number of space or tab characters match zero or more spaces
/\s+/g,'\\s*'
);

var matches = new RegExp('^'+pattern_out, "i").exec(UTF8ToString(buf))

function initDate() {
function fixup(value, min, max) {
return (typeof value != 'number' || isNaN(value)) ? min : (value>=min ? (value<=max ? value: max): min);
};
return {
year: fixup({{{ makeGetValue('tm', C_STRUCTS.tm.tm_year, 'i32') }}} + 1900 , 1970, 9999),
month: fixup({{{ makeGetValue('tm', C_STRUCTS.tm.tm_mon, 'i32') }}}, 0, 11),
day: fixup({{{ makeGetValue('tm', C_STRUCTS.tm.tm_mday, 'i32') }}}, 1, 31),
hour: fixup({{{ makeGetValue('tm', C_STRUCTS.tm.tm_hour, 'i32') }}}, 0, 23),
min: fixup({{{ makeGetValue('tm', C_STRUCTS.tm.tm_min, 'i32') }}}, 0, 59),
sec: fixup({{{ makeGetValue('tm', C_STRUCTS.tm.tm_sec, 'i32') }}}, 0, 59),
gmtoff: 0
};
};

if (matches) {
var date = initDate();
var value;

var getMatch = (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 = jstoi_q(value);
}

// minutes
if ((value=getMatch('M'))) {
date.min = jstoi_q(value);
}

// hours
if ((value=getMatch('H'))) {
// 24h clock
date.hour = jstoi_q(value);
} else if ((value = getMatch('I'))) {
// AM/PM clock
var hour = jstoi_q(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 = jstoi_q(value);
} else if ((value=getMatch('y'))) {
// parse from two-digit year...
var year = jstoi_q(value);
if ((value=getMatch('C'))) {
// ...and century
year += jstoi_q(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 = jstoi_q(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 = jstoi_q(value);
} else if ((value=getMatch('j'))) {
// get day of month from day of year ...
var day = jstoi_q(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 = jstoi_q(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 = jstoi_q(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();
}
}

// time zone
if ((value = getMatch('z'))) {
// GMT offset as either 'Z' or +-HH:MM or +-HH or +-HHMM
if (value.toLowerCase() === 'z'){
date.gmtoff = 0;
} else {
var match = value.match(/^((?:\-|\+)\d\d):?(\d\d)?/);
date.gmtoff = match[1] * 3600;
if (match[2]) {
date.gmtoff += date.gmtoff >0 ? match[2] * 60 : -match[2] * 60
}
}
}

/*
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
tm_gmtoff long offset from GMT (seconds)
*/

var fullDate = new Date(date.year, date.month, date.day, date.hour, date.min, date.sec, 0);
{{{ makeSetValue('tm', C_STRUCTS.tm.tm_sec, 'fullDate.getSeconds()', 'i32') }}};
{{{ makeSetValue('tm', C_STRUCTS.tm.tm_min, 'fullDate.getMinutes()', 'i32') }}};
{{{ makeSetValue('tm', C_STRUCTS.tm.tm_hour, 'fullDate.getHours()', 'i32') }}};
{{{ makeSetValue('tm', C_STRUCTS.tm.tm_mday, 'fullDate.getDate()', 'i32') }}};
{{{ makeSetValue('tm', C_STRUCTS.tm.tm_mon, 'fullDate.getMonth()', 'i32') }}};
{{{ makeSetValue('tm', C_STRUCTS.tm.tm_year, 'fullDate.getFullYear()-1900', 'i32') }}};
{{{ makeSetValue('tm', C_STRUCTS.tm.tm_wday, 'fullDate.getDay()', 'i32') }}};
{{{ makeSetValue('tm', C_STRUCTS.tm.tm_yday, 'arraySum(isLeapYear(fullDate.getFullYear()) ? MONTH_DAYS_LEAP : MONTH_DAYS_REGULAR, fullDate.getMonth()-1)+fullDate.getDate()-1', 'i32') }}};
{{{ makeSetValue('tm', C_STRUCTS.tm.tm_isdst, '0', 'i32') }}};
{{{ makeSetValue('tm', C_STRUCTS.tm.tm_gmtoff, 'date.gmtoff', LONG_TYPE) }}};

// 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__deps: ['strptime'],
strptime_l: (buf, format, tm, locale) => {
return _strptime(buf, format, tm); // no locale support yet
},

// ==========================================================================
// setjmp.h
// ==========================================================================
Expand Down
2 changes: 0 additions & 2 deletions src/library_sigs.js
Original file line number Diff line number Diff line change
Expand Up @@ -1536,8 +1536,6 @@ sigs = {
rotozoomSurface__sig: 'ppddi',
sched_yield__sig: 'i',
setprotoent__sig: 'vi',
strptime__sig: 'pppp',
strptime_l__sig: 'ppppp',
uuid_clear__sig: 'vp',
uuid_compare__sig: 'ipp',
uuid_copy__sig: 'vpp',
Expand Down
Loading
Loading