PHP's strptime in JavaScript

Here’s what our current JavaScript equivalent to PHP's strptime looks like.

module.exports = function strptime (dateStr, format) {
// discuss at: https://locutus.io/php/strptime/
// original by: Brett Zamir (https://brett-zamir.me)
// original by: strftime
// example 1: strptime('20091112222135', '%Y%m%d%H%M%S') // Return value will depend on date and locale
// returns 1: {tm_sec: 35, tm_min: 21, tm_hour: 22, tm_mday: 12, tm_mon: 10, tm_year: 109, tm_wday: 4, tm_yday: 315, unparsed: ''}
// example 2: strptime('2009extra', '%Y')
// returns 2: {tm_sec:0, tm_min:0, tm_hour:0, tm_mday:0, tm_mon:0, tm_year:109, tm_wday:3, tm_yday: -1, unparsed: 'extra'}
const setlocale = require('../strings/setlocale')
const arrayMap = require('../array/array_map')
const retObj = {
tm_sec: 0,
tm_min: 0,
tm_hour: 0,
tm_mday: 0,
tm_mon: 0,
tm_year: 0,
tm_wday: 0,
tm_yday: 0,
unparsed: ''
}
let i = 0
let j = 0
let amPmOffset = 0
let prevHour = false
const _reset = function (dateObj, realMday) {
// realMday is to allow for a value of 0 in return results (but without
// messing up the Date() object)
let jan1
const o = retObj
const d = dateObj
o.tm_sec = d.getUTCSeconds()
o.tm_min = d.getUTCMinutes()
o.tm_hour = d.getUTCHours()
o.tm_mday = realMday === 0 ? realMday : d.getUTCDate()
o.tm_mon = d.getUTCMonth()
o.tm_year = d.getUTCFullYear() - 1900
o.tm_wday = realMday === 0 ? (d.getUTCDay() > 0 ? d.getUTCDay() - 1 : 6) : d.getUTCDay()
jan1 = new Date(Date.UTC(d.getUTCFullYear(), 0, 1))
o.tm_yday = Math.ceil((d - jan1) / (1000 * 60 * 60 * 24))
}
const _date = function () {
const o = retObj
// We set date to at least 1 to ensure year or month doesn't go backwards
return _reset(new Date(Date.UTC(
o.tm_year + 1900,
o.tm_mon,
o.tm_mday || 1,
o.tm_hour,
o.tm_min,
o.tm_sec
)),
o.tm_mday)
}
const _NWS = /\S/
const _WS = /\s/
const _aggregates = {
c: 'locale',
D: '%m/%d/%y',
F: '%y-%m-%d',
r: 'locale',
R: '%H:%M',
T: '%H:%M:%S',
x: 'locale',
X: 'locale'
}
/* Fix: Locale alternatives are supported though not documented in PHP; see https://linux.die.net/man/3/strptime
Ec
EC
Ex
EX
Ey
EY
Od or Oe
OH
OI
Om
OM
OS
OU
Ow
OW
Oy
*/
const _pregQuote = function (str) {
return (str + '').replace(/([\\.+*?[^\]$(){}=!<>|:])/g, '\\$1')
}
// ensure setup of localization variables takes place
setlocale('LC_ALL', 0)
const $global = (typeof window !== 'undefined' ? window : global)
$global.$locutus = $global.$locutus || {}
const $locutus = $global.$locutus
const locale = $locutus.php.localeCategories.LC_TIME
const lcTime = $locutus.php.locales[locale].LC_TIME
// First replace aggregates (run in a loop because an agg may be made up of other aggs)
while (format.match(/%[cDFhnrRtTxX]/)) {
format = format.replace(/%([cDFhnrRtTxX])/g, function (m0, m1) {
const f = _aggregates[m1]
return (f === 'locale' ? lcTime[m1] : f)
})
}
const _addNext = function (j, regex, cb) {
if (typeof regex === 'string') {
regex = new RegExp('^' + regex, 'i')
}
const check = dateStr.slice(j)
const match = regex.exec(check)
// Even if the callback returns null after assigning to the
// return object, the object won't be saved anyways
const testNull = match ? cb.apply(null, match) : null
if (testNull === null) {
throw new Error('No match in string')
}
return j + match[0].length
}
const _addLocalized = function (j, formatChar, category) {
// Could make each parenthesized instead and pass index to callback:
return _addNext(j, arrayMap(_pregQuote, lcTime[formatChar]).join('|'),
function (m) {
const match = lcTime[formatChar].search(new RegExp('^' + _pregQuote(m) + '$', 'i'))
if (match) {
retObj[category] = match[0]
}
})
}
// BEGIN PROCESSING CHARACTERS
for (i = 0, j = 0; i < format.length; i++) {
if (format.charAt(i) === '%') {
const literalPos = ['%', 'n', 't'].indexOf(format.charAt(i + 1))
if (literalPos !== -1) {
if (['%', '\n', '\t'].indexOf(dateStr.charAt(j)) === literalPos) {
// a matched literal
++i
// skip beyond
++j
continue
}
// Format indicated a percent literal, but not actually present
return false
}
var formatChar = format.charAt(i + 1)
try {
switch (formatChar) {
case 'a':
case 'A':
// Sunday-Saturday
// Changes nothing else
j = _addLocalized(j, formatChar, 'tm_wday')
break
case 'h':
case 'b':
// Jan-Dec
j = _addLocalized(j, 'b', 'tm_mon')
// Also changes wday, yday
_date()
break
case 'B':
// January-December
j = _addLocalized(j, formatChar, 'tm_mon')
// Also changes wday, yday
_date()
break
case 'C':
// 0+; century (19 for 20th)
// PHP docs say two-digit, but accepts one-digit (two-digit max):
j = _addNext(j, /^\d?\d/,
function (d) {
const year = (parseInt(d, 10) - 19) * 100
retObj.tm_year = year
_date()
if (!retObj.tm_yday) {
retObj.tm_yday = -1
}
// Also changes wday; and sets yday to -1 (always?)
})
break
case 'd':
case 'e':
// 1-31 day
j = _addNext(j, formatChar === 'd'
? /^(0[1-9]|[1-2]\d|3[0-1])/
: /^([1-2]\d|3[0-1]|[1-9])/,
function (d) {
const dayMonth = parseInt(d, 10)
retObj.tm_mday = dayMonth
// Also changes w_day, y_day
_date()
})
break
case 'g':
// No apparent effect; 2-digit year (see 'V')
break
case 'G':
// No apparent effect; 4-digit year (see 'V')'
break
case 'H':
// 00-23 hours
j = _addNext(j, /^([0-1]\d|2[0-3])/, function (d) {
const hour = parseInt(d, 10)
retObj.tm_hour = hour
// Changes nothing else
})
break
case 'l':
case 'I':
// 01-12 hours
j = _addNext(j, formatChar === 'l'
? /^([1-9]|1[0-2])/
: /^(0[1-9]|1[0-2])/,
function (d) {
const hour = parseInt(d, 10) - 1 + amPmOffset
retObj.tm_hour = hour
// Used for coordinating with am-pm
prevHour = true
// Changes nothing else, but affected by prior 'p/P'
})
break
case 'j':
// 001-366 day of year
j = _addNext(j, /^(00[1-9]|0[1-9]\d|[1-2]\d\d|3[0-6][0-6])/, function (d) {
const dayYear = parseInt(d, 10) - 1
retObj.tm_yday = dayYear
// Changes nothing else
// (oddly, since if original by a given year, could calculate other fields)
})
break
case 'm':
// 01-12 month
j = _addNext(j, /^(0[1-9]|1[0-2])/, function (d) {
const month = parseInt(d, 10) - 1
retObj.tm_mon = month
// Also sets wday and yday
_date()
})
break
case 'M':
// 00-59 minutes
j = _addNext(j, /^[0-5]\d/, function (d) {
const minute = parseInt(d, 10)
retObj.tm_min = minute
// Changes nothing else
})
break
case 'P':
// Seems not to work; AM-PM
// Could make fall-through instead since supposed to be a synonym despite PHP docs
return false
case 'p':
// am-pm
j = _addNext(j, /^(am|pm)/i, function (d) {
// No effect on 'H' since already 24 hours but
// works before or after setting of l/I hour
amPmOffset = /a/.test(d) ? 0 : 12
if (prevHour) {
retObj.tm_hour += amPmOffset
}
})
break
case 's':
// Unix timestamp (in seconds)
j = _addNext(j, /^\d+/, function (d) {
const timestamp = parseInt(d, 10)
const date = new Date(Date.UTC(timestamp * 1000))
_reset(date)
// Affects all fields, but can't be negative (and initial + not allowed)
})
break
case 'S':
// 00-59 seconds
j = _addNext(j, /^[0-5]\d/, // strptime also accepts 60-61 for some reason
function (d) {
const second = parseInt(d, 10)
retObj.tm_sec = second
// Changes nothing else
})
break
case 'u':
case 'w':
// 0 (Sunday)-6(Saturday)
j = _addNext(j, /^\d/, function (d) {
retObj.tm_wday = d - (formatChar === 'u')
// Changes nothing else apparently
})
break
case 'U':
case 'V':
case 'W':
// Apparently ignored (week of year, from 1st Monday)
break
case 'y':
// 69 (or higher) for 1969+, 68 (or lower) for 2068-
// PHP docs say two-digit, but accepts one-digit (two-digit max):
j = _addNext(j, /^\d?\d/,
function (d) {
d = parseInt(d, 10)
const year = d >= 69 ? d : d + 100
retObj.tm_year = year
_date()
if (!retObj.tm_yday) {
retObj.tm_yday = -1
}
// Also changes wday; and sets yday to -1 (always?)
})
break
case 'Y':
// 2010 (4-digit year)
// PHP docs say four-digit, but accepts one-digit (four-digit max):
j = _addNext(j, /^\d{1,4}/,
function (d) {
const year = (parseInt(d, 10)) - 1900
retObj.tm_year = year
_date()
if (!retObj.tm_yday) {
retObj.tm_yday = -1
}
// Also changes wday; and sets yday to -1 (always?)
})
break
case 'z':
// Timezone; on my system, strftime gives -0800,
// but strptime seems not to alter hour setting
break
case 'Z':
// Timezone; on my system, strftime gives PST, but strptime treats text as unparsed
break
default:
throw new Error('Unrecognized formatting character in strptime()')
}
} catch (e) {
if (e === 'No match in string') {
// Allow us to exit
// There was supposed to be a matching format but there wasn't
return false
}
// Calculate skipping beyond initial percent too
}
++i
} else if (format.charAt(i) !== dateStr.charAt(j)) {
// If extra whitespace at beginning or end of either, or between formats, no problem
// (just a problem when between % and format specifier)
// If the string has white-space, it is ok to ignore
if (dateStr.charAt(j).search(_WS) !== -1) {
j++
// Let the next iteration try again with the same format character
i--
} else if (format.charAt(i).search(_NWS) !== -1) {
// Any extra formatting characters besides white-space causes
// problems (do check after WS though, as may just be WS in string before next character)
return false
}
// Extra WS in format
// Adjust strings when encounter non-matching whitespace, so they align in future checks above
// Will check on next iteration (against same (non-WS) string character)
} else {
j++
}
}
// POST-PROCESSING
// Will also get extra whitespace; empty string if none
retObj.unparsed = dateStr.slice(j)
return retObj
}
[ View on GitHub | Edit on GitHub | Source on GitHub ]

How to use

You you can install via npm install locutus and require it via require('locutus/php/datetime/strptime'). You could also require the datetime module in full so that you could access datetime.strptime instead.

If you intend to target the browser, you can then use a module bundler such as Parcel, webpack, Browserify, or rollup.js. This can be important because Locutus allows modern JavaScript in the source files, meaning it may not work in all browsers without a build/transpile step. Locutus does transpile all functions to ES5 before publishing to npm.

A community effort

Not unlike Wikipedia, Locutus is an ongoing community effort. Our philosophy follows The McDonald’s Theory. This means that we don't consider it to be a bad thing that many of our functions are first iterations, which may still have their fair share of issues. We hope that these flaws will inspire others to come up with better ideas.

This way of working also means that we don't offer any production guarantees, and recommend to use Locutus inspiration and learning purposes only.

Examples

Please note that these examples are distilled from test cases that automatically verify our functions still work correctly. This could explain some quirky ones.

#codeexpected result
1strptime('20091112222135', '%Y%m%d%H%M%S') // Return value will depend on date and locale{tm_sec: 35, tm_min: 21, tm_hour: 22, tm_mday: 12, tm_mon: 10, tm_year: 109, tm_wday: 4, tm_yday: 315, unparsed: ''}
2strptime('2009extra', '%Y'){tm_sec:0, tm_min:0, tm_hour:0, tm_mday:0, tm_mon:0, tm_year:109, tm_wday:3, tm_yday: -1, unparsed: 'extra'}

« More PHP datetime functions


Star