locutus
Version:
Locutus other languages' standard libraries to JavaScript for fun and educational purposes
1,271 lines (1,270 loc) • 39.6 kB
JavaScript
const reSpace = '[ \\t]+';
const reSpaceOpt = '[ \\t]*';
const reMeridian = '(?:([ap])\\.?m\\.?([\\t ]|$))';
const reHour24 = '(2[0-4]|[01]?[0-9])';
const reHour24lz = '([01][0-9]|2[0-4])';
const reHour12 = '(0?[1-9]|1[0-2])';
const reMinute = '([0-5]?[0-9])';
const reMinutelz = '([0-5][0-9])';
const reSecond = '(60|[0-5]?[0-9])';
const reSecondlz = '(60|[0-5][0-9])';
const reFrac = '(?:\\.([0-9]+))';
const reDayfull = 'sunday|monday|tuesday|wednesday|thursday|friday|saturday';
const reDayabbr = 'sun|mon|tue|wed|thu|fri|sat';
const reDaytext = reDayfull + '|' + reDayabbr + '|weekdays?';
const reReltextnumber = 'first|second|third|fourth|fifth|sixth|seventh|eighth?|ninth|tenth|eleventh|twelfth';
const reReltexttext = 'next|last|previous|this';
const reReltextunit = '(?:second|sec|minute|min|hour|day|fortnight|forthnight|month|year)s?|weeks|' + reDaytext;
const reYear = '([0-9]{1,4})';
const reYear2 = '([0-9]{2})';
const reYear4 = '([0-9]{4})';
const reYear4withSign = '([+-]?[0-9]{4})';
const reMonth = '(1[0-2]|0?[0-9])';
const reMonthlz = '(0[0-9]|1[0-2])';
const reDay = '(?:(3[01]|[0-2]?[0-9])(?:st|nd|rd|th)?)';
const reDaylz = '(0[0-9]|[1-2][0-9]|3[01])';
const reMonthFull = 'january|february|march|april|may|june|july|august|september|october|november|december';
const reMonthAbbr = 'jan|feb|mar|apr|may|jun|jul|aug|sept?|oct|nov|dec';
const reMonthroman = 'i[vx]|vi{0,3}|xi{0,2}|i{1,3}';
const reMonthText = '(' + reMonthFull + '|' + reMonthAbbr + '|' + reMonthroman + ')';
const reTzCorrection = '((?:GMT)?([+-])' + reHour24 + ':?' + reMinute + '?)';
const reTzAbbr = '\\(?([a-zA-Z]{1,6})\\)?';
const reDayOfYear = '(00[1-9]|0[1-9][0-9]|[12][0-9][0-9]|3[0-5][0-9]|36[0-6])';
const reWeekOfYear = '(0[1-9]|[1-4][0-9]|5[0-3])';
const reDateNoYear = reMonthText + '[ .\\t-]*' + reDay + '[,.stndrh\\t ]*';
function processMeridian(hour, meridian) {
const normalizedMeridian = meridian?.toLowerCase();
switch (normalizedMeridian) {
case 'a':
hour += hour === 12 ? -12 : 0;
break;
case 'p':
hour += hour !== 12 ? 12 : 0;
break;
}
return hour;
}
function processYear(yearStr) {
let year = +yearStr;
if (yearStr.length < 4 && year < 100) {
year += year < 70 ? 2000 : 1900;
}
return year;
}
function lookupMonth(monthStr) {
const monthLookup = {
jan: 0,
january: 0,
i: 0,
feb: 1,
february: 1,
ii: 1,
mar: 2,
march: 2,
iii: 2,
apr: 3,
april: 3,
iv: 3,
may: 4,
v: 4,
jun: 5,
june: 5,
vi: 5,
jul: 6,
july: 6,
vii: 6,
aug: 7,
august: 7,
viii: 7,
sep: 8,
sept: 8,
september: 8,
ix: 8,
oct: 9,
october: 9,
x: 9,
nov: 10,
november: 10,
xi: 10,
dec: 11,
december: 11,
xii: 11,
};
return monthLookup[monthStr.toLowerCase()] ?? Number.NaN;
}
function lookupWeekday(dayStr, desiredSundayNumber = 0) {
const dayNumbers = {
mon: 1,
monday: 1,
tue: 2,
tuesday: 2,
wed: 3,
wednesday: 3,
thu: 4,
thursday: 4,
fri: 5,
friday: 5,
sat: 6,
saturday: 6,
sun: 0,
sunday: 0,
};
return dayNumbers[dayStr.toLowerCase()] || desiredSundayNumber;
}
function lookupRelative(relText) {
const relativeNumbers = {
last: -1,
previous: -1,
this: 0,
first: 1,
next: 1,
second: 2,
third: 3,
fourth: 4,
fifth: 5,
sixth: 6,
seventh: 7,
eight: 8,
eighth: 8,
ninth: 9,
tenth: 10,
eleventh: 11,
twelfth: 12,
};
const relativeBehavior = {
this: 1,
};
const relTextLower = relText.toLowerCase();
return {
amount: relativeNumbers[relTextLower] ?? 0,
behavior: relativeBehavior[relTextLower] || 0,
};
}
function processTzCorrection(tzOffset, oldValue = Number.NaN) {
const reTzCorrectionLoose = /(?:GMT)?([+-])(\d+)(:?)(\d{0,2})/i;
const tzMatch = tzOffset?.match(reTzCorrectionLoose);
if (!tzMatch) {
return oldValue;
}
const sign = tzMatch[1] === '-' ? -1 : 1;
let hours = +(tzMatch[2] ?? 0);
let minutes = +(tzMatch[4] ?? 0);
if (!tzMatch[4] && !tzMatch[3]) {
minutes = Math.floor(hours % 100);
hours = Math.floor(hours / 100);
}
// timezone offset in seconds
return sign * (hours * 60 + minutes) * 60;
}
// tz abbrevation : tz offset in seconds
const tzAbbrOffsets = {
acdt: 37800,
acst: 34200,
addt: -7200,
adt: -10800,
aedt: 39600,
aest: 36000,
ahdt: -32400,
ahst: -36000,
akdt: -28800,
akst: -32400,
amt: -13840,
apt: -10800,
ast: -14400,
awdt: 32400,
awst: 28800,
awt: -10800,
bdst: 7200,
bdt: -36000,
bmt: -14309,
bst: 3600,
cast: 34200,
cat: 7200,
cddt: -14400,
cdt: -18000,
cemt: 10800,
cest: 7200,
cet: 3600,
cmt: -15408,
cpt: -18000,
cst: -21600,
cwt: -18000,
chst: 36000,
dmt: -1521,
eat: 10800,
eddt: -10800,
edt: -14400,
eest: 10800,
eet: 7200,
emt: -26248,
ept: -14400,
est: -18000,
ewt: -14400,
ffmt: -14660,
fmt: -4056,
gdt: 39600,
gmt: 0,
gst: 36000,
hdt: -34200,
hkst: 32400,
hkt: 28800,
hmt: -19776,
hpt: -34200,
hst: -36000,
hwt: -34200,
iddt: 14400,
idt: 10800,
imt: 25025,
ist: 7200,
jdt: 36000,
jmt: 8440,
jst: 32400,
kdt: 36000,
kmt: 5736,
kst: 30600,
lst: 9394,
mddt: -18000,
mdst: 16279,
mdt: -21600,
mest: 7200,
met: 3600,
mmt: 9017,
mpt: -21600,
msd: 14400,
msk: 10800,
mst: -25200,
mwt: -21600,
nddt: -5400,
ndt: -9052,
npt: -9000,
nst: -12600,
nwt: -9000,
nzdt: 46800,
nzmt: 41400,
nzst: 43200,
pddt: -21600,
pdt: -25200,
pkst: 21600,
pkt: 18000,
plmt: 25590,
pmt: -13236,
ppmt: -17340,
ppt: -25200,
pst: -28800,
pwt: -25200,
qmt: -18840,
rmt: 5794,
sast: 7200,
sdmt: -16800,
sjmt: -20173,
smt: -13884,
sst: -39600,
tbmt: 10751,
tmt: 12344,
uct: 0,
utc: 0,
wast: 7200,
wat: 3600,
wemt: 7200,
west: 3600,
wet: 0,
wib: 25200,
wita: 28800,
wit: 32400,
wmt: 5040,
yddt: -25200,
ydt: -28800,
ypt: -28800,
yst: -32400,
ywt: -28800,
a: 3600,
b: 7200,
c: 10800,
d: 14400,
e: 18000,
f: 21600,
g: 25200,
h: 28800,
i: 32400,
k: 36000,
l: 39600,
m: 43200,
n: -3600,
o: -7200,
p: -10800,
q: -14400,
r: -18000,
s: -21600,
t: -25200,
u: -28800,
v: -32400,
w: -36000,
x: -39600,
y: -43200,
z: 0,
};
const formats = {
yesterday: {
regex: /^yesterday/i,
name: 'yesterday',
callback() {
this.rd -= 1;
return this.resetTime();
},
},
now: {
regex: /^now/i,
name: 'now',
// do nothing
},
noon: {
regex: /^noon/i,
name: 'noon',
callback() {
return this.resetTime() && this.time(12, 0, 0, 0);
},
},
midnightOrToday: {
regex: /^(midnight|today)/i,
name: 'midnight | today',
callback() {
return this.resetTime();
},
},
tomorrow: {
regex: /^tomorrow/i,
name: 'tomorrow',
callback() {
this.rd += 1;
return this.resetTime();
},
},
timestamp: {
regex: /^@(-?\d+)/i,
name: 'timestamp',
callback(match, timestamp) {
this.rs += +timestamp;
this.y = 1970;
this.m = 0;
this.d = 1;
this.dates = 0;
return this.resetTime() && this.zone(0);
},
},
firstOrLastDay: {
regex: /^(first|last) day of/i,
name: 'firstdayof | lastdayof',
callback(match, day) {
if (day.toLowerCase() === 'first') {
this.firstOrLastDayOfMonth = 1;
}
else {
this.firstOrLastDayOfMonth = -1;
}
},
},
backOrFrontOf: {
regex: new RegExp('^(back|front) of ' + reHour24 + reSpaceOpt + reMeridian + '?', 'i'),
name: 'backof | frontof',
callback(match, side, hours, meridian) {
const back = side.toLowerCase() === 'back';
let hour = +hours;
let minute = 15;
if (!back) {
hour -= 1;
minute = 45;
}
hour = processMeridian(hour, meridian);
return this.resetTime() && this.time(hour, minute, 0, 0);
},
},
weekdayOf: {
regex: new RegExp('^(' +
reReltextnumber +
'|' +
reReltexttext +
')' +
reSpace +
'(' +
reDayfull +
'|' +
reDayabbr +
')' +
reSpace +
'of', 'i'),
name: 'weekdayof',
// todo
},
mssqltime: {
regex: new RegExp('^' + reHour12 + ':' + reMinutelz + ':' + reSecondlz + '[:.]([0-9]+)' + reMeridian, 'i'),
name: 'mssqltime',
callback(match, hour, minute, second, frac, meridian) {
return this.time(processMeridian(+hour, meridian), +minute, +second, +frac.substr(0, 3));
},
},
oracledate: {
regex: /^(\d{2})-([A-Z]{3})-(\d{2})$/i,
name: 'd-M-y',
callback(match, day, monthText, year) {
const monthByName = {
JAN: 0,
FEB: 1,
MAR: 2,
APR: 3,
MAY: 4,
JUN: 5,
JUL: 6,
AUG: 7,
SEP: 8,
OCT: 9,
NOV: 10,
DEC: 11,
};
const month = monthByName[monthText.toUpperCase()] ?? Number.NaN;
return this.ymd(2000 + parseInt(year, 10), month, parseInt(day, 10));
},
},
timeLong12: {
regex: new RegExp('^' + reHour12 + '[:.]' + reMinute + '[:.]' + reSecondlz + reSpaceOpt + reMeridian, 'i'),
name: 'timelong12',
callback(match, hour, minute, second, meridian) {
return this.time(processMeridian(+hour, meridian), +minute, +second, 0);
},
},
timeShort12: {
regex: new RegExp('^' + reHour12 + '[:.]' + reMinutelz + reSpaceOpt + reMeridian, 'i'),
name: 'timeshort12',
callback(match, hour, minute, meridian) {
return this.time(processMeridian(+hour, meridian), +minute, 0, 0);
},
},
timeTiny12: {
regex: new RegExp('^' + reHour12 + reSpaceOpt + reMeridian, 'i'),
name: 'timetiny12',
callback(match, hour, meridian) {
return this.time(processMeridian(+hour, meridian), 0, 0, 0);
},
},
soap: {
regex: new RegExp('^' +
reYear4 +
'-' +
reMonthlz +
'-' +
reDaylz +
'T' +
reHour24lz +
':' +
reMinutelz +
':' +
reSecondlz +
reFrac +
reTzCorrection +
'?', 'i'),
name: 'soap',
callback(match, year, month, day, hour, minute, second, frac, tzCorrection) {
return (this.ymd(+year, +month - 1, +day) &&
this.time(+hour, +minute, +second, +frac.substr(0, 3)) &&
this.zone(processTzCorrection(tzCorrection)));
},
},
wddx: {
regex: new RegExp('^' + reYear4 + '-' + reMonth + '-' + reDay + 'T' + reHour24 + ':' + reMinute + ':' + reSecond),
name: 'wddx',
callback(match, year, month, day, hour, minute, second) {
return this.ymd(+year, +month - 1, +day) && this.time(+hour, +minute, +second, 0);
},
},
exif: {
regex: new RegExp('^' + reYear4 + ':' + reMonthlz + ':' + reDaylz + ' ' + reHour24lz + ':' + reMinutelz + ':' + reSecondlz, 'i'),
name: 'exif',
callback(match, year, month, day, hour, minute, second) {
return this.ymd(+year, +month - 1, +day) && this.time(+hour, +minute, +second, 0);
},
},
xmlRpc: {
regex: new RegExp('^' + reYear4 + reMonthlz + reDaylz + 'T' + reHour24 + ':' + reMinutelz + ':' + reSecondlz),
name: 'xmlrpc',
callback(match, year, month, day, hour, minute, second) {
return this.ymd(+year, +month - 1, +day) && this.time(+hour, +minute, +second, 0);
},
},
xmlRpcNoColon: {
regex: new RegExp('^' + reYear4 + reMonthlz + reDaylz + '[Tt]' + reHour24 + reMinutelz + reSecondlz),
name: 'xmlrpcnocolon',
callback(match, year, month, day, hour, minute, second) {
return this.ymd(+year, +month - 1, +day) && this.time(+hour, +minute, +second, 0);
},
},
clf: {
regex: new RegExp('^' +
reDay +
'/(' +
reMonthAbbr +
')/' +
reYear4 +
':' +
reHour24lz +
':' +
reMinutelz +
':' +
reSecondlz +
reSpace +
reTzCorrection, 'i'),
name: 'clf',
callback(match, day, month, year, hour, minute, second, tzCorrection) {
return (this.ymd(+year, lookupMonth(month), +day) &&
this.time(+hour, +minute, +second, 0) &&
this.zone(processTzCorrection(tzCorrection)));
},
},
iso8601long: {
regex: new RegExp('^t?' + reHour24 + '[:.]' + reMinute + '[:.]' + reSecond + reFrac, 'i'),
name: 'iso8601long',
callback(match, hour, minute, second, frac) {
return this.time(+hour, +minute, +second, +frac.substr(0, 3));
},
},
dateTextual: {
regex: new RegExp('^' + reMonthText + '[ .\\t-]*' + reDay + '[,.stndrh\\t ]+' + reYear, 'i'),
name: 'datetextual',
callback(match, month, day, year) {
return this.ymd(processYear(year), lookupMonth(month), +day);
},
},
pointedDate4: {
regex: new RegExp('^' + reDay + '[.\\t-]' + reMonth + '[.-]' + reYear4),
name: 'pointeddate4',
callback(match, day, month, year) {
return this.ymd(+year, +month - 1, +day);
},
},
pointedDate2: {
regex: new RegExp('^' + reDay + '[.\\t]' + reMonth + '\\.' + reYear2),
name: 'pointeddate2',
callback(match, day, month, year) {
return this.ymd(processYear(year), +month - 1, +day);
},
},
timeLong24: {
regex: new RegExp('^t?' + reHour24 + '[:.]' + reMinute + '[:.]' + reSecond),
name: 'timelong24',
callback(match, hour, minute, second) {
return this.time(+hour, +minute, +second, 0);
},
},
dateNoColon: {
regex: new RegExp('^' + reYear4 + reMonthlz + reDaylz),
name: 'datenocolon',
callback(match, year, month, day) {
return this.ymd(+year, +month - 1, +day);
},
},
pgydotd: {
regex: new RegExp('^' + reYear4 + '\\.?' + reDayOfYear),
name: 'pgydotd',
callback(match, year, day) {
return this.ymd(+year, 0, +day);
},
},
timeShort24: {
regex: new RegExp('^t?' + reHour24 + '[:.]' + reMinute, 'i'),
name: 'timeshort24',
callback(match, hour, minute) {
return this.time(+hour, +minute, 0, 0);
},
},
iso8601noColon: {
regex: new RegExp('^t?' + reHour24lz + reMinutelz + reSecondlz, 'i'),
name: 'iso8601nocolon',
callback(match, hour, minute, second) {
return this.time(+hour, +minute, +second, 0);
},
},
iso8601dateSlash: {
// eventhough the trailing slash is optional in PHP
// here it's mandatory and inputs without the slash
// are handled by dateslash
regex: new RegExp('^' + reYear4 + '/' + reMonthlz + '/' + reDaylz + '/'),
name: 'iso8601dateslash',
callback(match, year, month, day) {
return this.ymd(+year, +month - 1, +day);
},
},
dateSlash: {
regex: new RegExp('^' + reYear4 + '/' + reMonth + '/' + reDay),
name: 'dateslash',
callback(match, year, month, day) {
return this.ymd(+year, +month - 1, +day);
},
},
american: {
regex: new RegExp('^' + reMonth + '/' + reDay + '/' + reYear),
name: 'american',
callback(match, month, day, year) {
return this.ymd(processYear(year), +month - 1, +day);
},
},
americanShort: {
regex: new RegExp('^' + reMonth + '/' + reDay),
name: 'americanshort',
callback(match, month, day) {
return this.ymd(this.y, +month - 1, +day);
},
},
gnuDateShortOrIso8601date2: {
// iso8601date2 is complete subset of gnudateshort
regex: new RegExp('^' + reYear + '-' + reMonth + '-' + reDay),
name: 'gnudateshort | iso8601date2',
callback(match, year, month, day) {
return this.ymd(processYear(year), +month - 1, +day);
},
},
iso8601date4: {
regex: new RegExp('^' + reYear4withSign + '-' + reMonthlz + '-' + reDaylz),
name: 'iso8601date4',
callback(match, year, month, day) {
return this.ymd(+year, +month - 1, +day);
},
},
gnuNoColon: {
regex: new RegExp('^t?' + reHour24lz + reMinutelz, 'i'),
name: 'gnunocolon',
callback(match, hour, minute) {
// this rule is a special case
// if time was already set once by any preceding rule, it sets the captured value as year
switch (this.times) {
case 0:
return this.time(+hour, +minute, 0, this.f);
case 1:
this.y = +hour * 100 + +minute;
this.times++;
return true;
default:
return false;
}
},
},
gnuDateShorter: {
regex: new RegExp('^' + reYear4 + '-' + reMonth),
name: 'gnudateshorter',
callback(match, year, month) {
return this.ymd(+year, +month - 1, 1);
},
},
pgTextReverse: {
// note: allowed years are from 32-9999
// years below 32 should be treated as days in datefull
regex: new RegExp('^' + '(\\d{3,4}|[4-9]\\d|3[2-9])-(' + reMonthAbbr + ')-' + reDaylz, 'i'),
name: 'pgtextreverse',
callback(match, year, month, day) {
return this.ymd(processYear(year), lookupMonth(month), +day);
},
},
dateFull: {
regex: new RegExp('^' + reDay + '[ \\t.-]*' + reMonthText + '[ \\t.-]*' + reYear, 'i'),
name: 'datefull',
callback(match, day, month, year) {
return this.ymd(processYear(year), lookupMonth(month), +day);
},
},
dateNoDay: {
regex: new RegExp('^' + reMonthText + '[ .\\t-]*' + reYear4, 'i'),
name: 'datenoday',
callback(match, month, year) {
return this.ymd(+year, lookupMonth(month), 1);
},
},
dateNoDayRev: {
regex: new RegExp('^' + reYear4 + '[ .\\t-]*' + reMonthText, 'i'),
name: 'datenodayrev',
callback(match, year, month) {
return this.ymd(+year, lookupMonth(month), 1);
},
},
pgTextShort: {
regex: new RegExp('^(' + reMonthAbbr + ')-' + reDaylz + '-' + reYear, 'i'),
name: 'pgtextshort',
callback(match, month, day, year) {
return this.ymd(processYear(year), lookupMonth(month), +day);
},
},
dateNoYear: {
regex: new RegExp('^' + reDateNoYear, 'i'),
name: 'datenoyear',
callback(match, month, day) {
return this.ymd(this.y, lookupMonth(month), +day);
},
},
dateNoYearRev: {
regex: new RegExp('^' + reDay + '[ .\\t-]*' + reMonthText, 'i'),
name: 'datenoyearrev',
callback(match, day, month) {
return this.ymd(this.y, lookupMonth(month), +day);
},
},
isoWeekDay: {
regex: new RegExp('^' + reYear4 + '-?W' + reWeekOfYear + '(?:-?([0-7]))?'),
name: 'isoweekday | isoweek',
callback(match, year, week, day) {
const dayOfWeek = day ? +day : 1;
if (!this.ymd(+year, 0, 1)) {
return false;
}
// get day of week for Jan 1st
let jan1DayOfWeek = new Date(this.y, this.m, this.d).getDay();
// and use the day to figure out the offset for day 1 of week 1
jan1DayOfWeek = 0 - (jan1DayOfWeek > 4 ? jan1DayOfWeek - 7 : jan1DayOfWeek);
this.rd += jan1DayOfWeek + (+week - 1) * 7 + dayOfWeek;
return true;
},
},
relativeText: {
regex: new RegExp('^(' + reReltextnumber + '|' + reReltexttext + ')' + reSpace + '(' + reReltextunit + ')', 'i'),
name: 'relativetext',
callback(match, relValue, relUnit) {
// todo: implement handling of 'this time-unit'
// biome-ignore lint/correctness/noUnusedVariables: behavior reserved for future use
const { amount, behavior } = lookupRelative(relValue);
switch (relUnit.toLowerCase()) {
case 'sec':
case 'secs':
case 'second':
case 'seconds':
this.rs += amount;
break;
case 'min':
case 'mins':
case 'minute':
case 'minutes':
this.ri += amount;
break;
case 'hour':
case 'hours':
this.rh += amount;
break;
case 'day':
case 'days':
this.rd += amount;
break;
case 'fortnight':
case 'fortnights':
case 'forthnight':
case 'forthnights':
this.rd += amount * 14;
break;
case 'week':
case 'weeks':
this.rd += amount * 7;
break;
case 'month':
case 'months':
this.rm += amount;
break;
case 'year':
case 'years':
this.ry += amount;
break;
case 'mon':
case 'monday':
case 'tue':
case 'tuesday':
case 'wed':
case 'wednesday':
case 'thu':
case 'thursday':
case 'fri':
case 'friday':
case 'sat':
case 'saturday':
case 'sun':
case 'sunday':
this.resetTime();
this.weekday = lookupWeekday(relUnit, 7);
this.weekdayBehavior = 1;
this.rd += (amount > 0 ? amount - 1 : amount) * 7;
break;
case 'weekday':
case 'weekdays':
// todo
break;
}
},
},
relative: {
regex: new RegExp('^([+-]*)[ \\t]*(\\d+)' + reSpaceOpt + '(' + reReltextunit + '|week)', 'i'),
name: 'relative',
callback(match, signs, relValue, relUnit) {
const minuses = signs.replace(/[^-]/g, '').length;
const amount = +relValue * Math.pow(-1, minuses);
switch (relUnit.toLowerCase()) {
case 'sec':
case 'secs':
case 'second':
case 'seconds':
this.rs += amount;
break;
case 'min':
case 'mins':
case 'minute':
case 'minutes':
this.ri += amount;
break;
case 'hour':
case 'hours':
this.rh += amount;
break;
case 'day':
case 'days':
this.rd += amount;
break;
case 'fortnight':
case 'fortnights':
case 'forthnight':
case 'forthnights':
this.rd += amount * 14;
break;
case 'week':
case 'weeks':
this.rd += amount * 7;
break;
case 'month':
case 'months':
this.rm += amount;
break;
case 'year':
case 'years':
this.ry += amount;
break;
case 'mon':
case 'monday':
case 'tue':
case 'tuesday':
case 'wed':
case 'wednesday':
case 'thu':
case 'thursday':
case 'fri':
case 'friday':
case 'sat':
case 'saturday':
case 'sun':
case 'sunday':
this.resetTime();
this.weekday = lookupWeekday(relUnit, 7);
this.weekdayBehavior = 1;
this.rd += (amount > 0 ? amount - 1 : amount) * 7;
break;
case 'weekday':
case 'weekdays':
// todo
break;
}
},
},
dayText: {
regex: new RegExp('^(' + reDaytext + ')', 'i'),
name: 'daytext',
callback(match, dayText) {
this.resetTime();
this.weekday = lookupWeekday(dayText, 0);
if (this.weekdayBehavior !== 2) {
this.weekdayBehavior = 1;
}
},
},
relativeTextWeek: {
regex: new RegExp('^(' + reReltexttext + ')' + reSpace + 'week', 'i'),
name: 'relativetextweek',
callback(match, relText) {
this.weekdayBehavior = 2;
switch (relText.toLowerCase()) {
case 'this':
this.rd += 0;
break;
case 'next':
this.rd += 7;
break;
case 'last':
case 'previous':
this.rd -= 7;
break;
}
if (isNaN(this.weekday)) {
this.weekday = 1;
}
},
},
monthFullOrMonthAbbr: {
regex: new RegExp('^(' + reMonthFull + '|' + reMonthAbbr + ')', 'i'),
name: 'monthfull | monthabbr',
callback(match, month) {
return this.ymd(this.y, lookupMonth(month), this.d);
},
},
tzCorrection: {
regex: new RegExp('^' + reTzCorrection, 'i'),
name: 'tzcorrection',
callback(tzCorrection) {
return this.zone(processTzCorrection(tzCorrection));
},
},
tzAbbr: {
regex: new RegExp('^' + reTzAbbr),
name: 'tzabbr',
callback(match, abbr) {
const offset = tzAbbrOffsets[abbr.toLowerCase()];
if (offset == null || Number.isNaN(offset)) {
return false;
}
return this.zone(offset);
},
},
ago: {
regex: /^ago/i,
name: 'ago',
callback() {
this.ry = -this.ry;
this.rm = -this.rm;
this.rd = -this.rd;
this.rh = -this.rh;
this.ri = -this.ri;
this.rs = -this.rs;
this.rf = -this.rf;
},
},
year4: {
regex: new RegExp('^' + reYear4),
name: 'year4',
callback(match, year) {
this.y = +year;
return true;
},
},
whitespace: {
regex: /^[ .,\t]+/,
name: 'whitespace',
// do nothing
},
dateShortWithTimeLong: {
regex: new RegExp('^' + reDateNoYear + 't?' + reHour24 + '[:.]' + reMinute + '[:.]' + reSecond, 'i'),
name: 'dateshortwithtimelong',
callback(match, month, day, hour, minute, second) {
return this.ymd(this.y, lookupMonth(month), +day) && this.time(+hour, +minute, +second, 0);
},
},
dateShortWithTimeLong12: {
regex: new RegExp('^' + reDateNoYear + reHour12 + '[:.]' + reMinute + '[:.]' + reSecondlz + reSpaceOpt + reMeridian, 'i'),
name: 'dateshortwithtimelong12',
callback(match, month, day, hour, minute, second, meridian) {
return (this.ymd(this.y, lookupMonth(month), +day) && this.time(processMeridian(+hour, meridian), +minute, +second, 0));
},
},
dateShortWithTimeShort: {
regex: new RegExp('^' + reDateNoYear + 't?' + reHour24 + '[:.]' + reMinute, 'i'),
name: 'dateshortwithtimeshort',
callback(match, month, day, hour, minute) {
return this.ymd(this.y, lookupMonth(month), +day) && this.time(+hour, +minute, 0, 0);
},
},
dateShortWithTimeShort12: {
regex: new RegExp('^' + reDateNoYear + reHour12 + '[:.]' + reMinutelz + reSpaceOpt + reMeridian, 'i'),
name: 'dateshortwithtimeshort12',
callback(match, month, day, hour, minute, meridian) {
return this.ymd(this.y, lookupMonth(month), +day) && this.time(processMeridian(+hour, meridian), +minute, 0, 0);
},
},
};
const resultProto = {
// date
y: NaN,
m: NaN,
d: NaN,
// time
h: NaN,
i: NaN,
s: NaN,
f: NaN,
// relative shifts
ry: 0,
rm: 0,
rd: 0,
rh: 0,
ri: 0,
rs: 0,
rf: 0,
// weekday related shifts
weekday: NaN,
weekdayBehavior: 0,
// first or last day of month
// 0 none, 1 first, -1 last
firstOrLastDayOfMonth: 0,
// timezone correction in minutes
z: NaN,
// counters
dates: 0,
times: 0,
zones: 0,
// helper functions
ymd(y, m, d) {
if (this.dates > 0) {
return false;
}
this.dates++;
this.y = y;
this.m = m;
this.d = d;
return true;
},
time(h, i, s, f) {
if (this.times > 0) {
return false;
}
this.times++;
this.h = h;
this.i = i;
this.s = s;
this.f = f;
return true;
},
resetTime() {
this.h = 0;
this.i = 0;
this.s = 0;
this.f = 0;
this.times = 0;
return true;
},
zone(minutes) {
if (this.zones <= 1) {
this.zones++;
this.z = minutes;
return true;
}
return false;
},
toDate(relativeTo) {
if (this.dates && !this.times) {
this.h = this.i = this.s = this.f = 0;
}
// fill holes
if (isNaN(this.y)) {
this.y = relativeTo.getFullYear();
}
if (isNaN(this.m)) {
this.m = relativeTo.getMonth();
}
if (isNaN(this.d)) {
this.d = relativeTo.getDate();
}
if (isNaN(this.h)) {
this.h = relativeTo.getHours();
}
if (isNaN(this.i)) {
this.i = relativeTo.getMinutes();
}
if (isNaN(this.s)) {
this.s = relativeTo.getSeconds();
}
if (isNaN(this.f)) {
this.f = relativeTo.getMilliseconds();
}
// adjust special early
switch (this.firstOrLastDayOfMonth) {
case 1:
this.d = 1;
break;
case -1:
this.d = 0;
this.m += 1;
break;
}
if (!isNaN(this.weekday)) {
const date = new Date(relativeTo.getTime());
date.setFullYear(this.y, this.m, this.d);
date.setHours(this.h, this.i, this.s, this.f);
const dow = date.getDay();
if (this.weekdayBehavior === 2) {
// To make "this week" work, where the current day of week is a "sunday"
if (dow === 0 && this.weekday !== 0) {
this.weekday = -6;
}
// To make "sunday this week" work, where the current day of week is not a "sunday"
if (this.weekday === 0 && dow !== 0) {
this.weekday = 7;
}
this.d -= dow;
this.d += this.weekday;
}
else {
let diff = this.weekday - dow;
// some PHP magic
if ((this.rd < 0 && diff < 0) || (this.rd >= 0 && diff <= -this.weekdayBehavior)) {
diff += 7;
}
if (this.weekday >= 0) {
this.d += diff;
}
else {
this.d -= 7 - (Math.abs(this.weekday) - dow);
}
this.weekday = NaN;
}
}
// adjust relative
this.y += this.ry;
this.m += this.rm;
this.d += this.rd;
this.h += this.rh;
this.i += this.ri;
this.s += this.rs;
this.f += this.rf;
this.ry = this.rm = this.rd = 0;
this.rh = this.ri = this.rs = this.rf = 0;
const result = new Date(relativeTo.getTime());
// since Date constructor treats years <= 99 as 1900+
// it can't be used, thus this weird way
result.setFullYear(this.y, this.m, this.d);
result.setHours(this.h, this.i, this.s, this.f);
// note: this is done twice in PHP
// early when processing special relatives
// and late
// todo: check if the logic can be reduced
// to just one time action
switch (this.firstOrLastDayOfMonth) {
case 1:
result.setDate(1);
break;
case -1:
result.setMonth(result.getMonth() + 1, 0);
break;
}
// adjust timezone
if (!isNaN(this.z) && result.getTimezoneOffset() !== this.z) {
result.setUTCFullYear(result.getFullYear(), result.getMonth(), result.getDate());
result.setUTCHours(result.getHours(), result.getMinutes(), result.getSeconds() - this.z, result.getMilliseconds());
}
return result;
},
};
export function strtotime(str, now) {
// discuss at: https://locutus.io/php/strtotime/
// original by: Caio Ariede (https://caioariede.com)
// improved by: Kevin van Zonneveld (https://kvz.io)
// improved by: Caio Ariede (https://caioariede.com)
// improved by: A. Matías Quezada (https://amatiasq.com)
// improved by: preuter
// improved by: Brett Zamir (https://brett-zamir.me)
// improved by: Mirko Faber
// input by: David
// bugfixed by: Wagner B. Soares
// bugfixed by: Artur Tchernychev
// bugfixed by: Stephan Bösch-Plepelits (https://github.com/plepe)
// reimplemented by: Rafał Kukawski
// note 1: Examples all have a fixed timestamp to prevent
// note 1: tests to fail because of variable time(zones)
// example 1: strtotime('+1 day', 1129633200)
// returns 1: 1129719600
// example 2: strtotime('+1 week 2 days 4 hours 2 seconds', 1129633200)
// returns 2: 1130425202
// example 3: strtotime('last month', 1129633200)
// returns 3: 1127041200
// example 4: strtotime('2009-05-04 08:30:00+00')
// returns 4: 1241425800
// example 5: strtotime('2009-05-04 08:30:00+02:00')
// returns 5: 1241418600
// example 6: strtotime('2009-05-04 08:30:00 YWT')
// returns 6: 1241454600
// example 7: strtotime('10-JUL-17')
// returns 7: 1499644800
const nowSeconds = now == null ? Math.floor(Date.now() / 1000) : now;
str = String(str);
// the rule order is important
// if multiple rules match, the longest match wins
// if multiple rules match the same string, the first match wins
const rules = [
formats.yesterday,
formats.now,
formats.noon,
formats.midnightOrToday,
formats.tomorrow,
formats.timestamp,
formats.firstOrLastDay,
formats.backOrFrontOf,
// formats.weekdayOf, // not yet implemented
formats.timeTiny12,
formats.timeShort12,
formats.timeLong12,
formats.mssqltime,
formats.oracledate,
formats.timeShort24,
formats.timeLong24,
formats.iso8601long,
formats.gnuNoColon,
formats.iso8601noColon,
formats.americanShort,
formats.american,
formats.iso8601date4,
formats.iso8601dateSlash,
formats.dateSlash,
formats.gnuDateShortOrIso8601date2,
formats.gnuDateShorter,
formats.dateFull,
formats.pointedDate4,
formats.pointedDate2,
formats.dateNoDay,
formats.dateNoDayRev,
formats.dateTextual,
formats.dateNoYear,
formats.dateNoYearRev,
formats.dateNoColon,
formats.xmlRpc,
formats.xmlRpcNoColon,
formats.soap,
formats.wddx,
formats.exif,
formats.pgydotd,
formats.isoWeekDay,
formats.pgTextShort,
formats.pgTextReverse,
formats.clf,
formats.year4,
formats.ago,
formats.dayText,
formats.relativeTextWeek,
formats.relativeText,
formats.monthFullOrMonthAbbr,
formats.tzCorrection,
formats.tzAbbr,
formats.dateShortWithTimeShort12,
formats.dateShortWithTimeLong12,
formats.dateShortWithTimeShort,
formats.dateShortWithTimeLong,
formats.relative,
formats.whitespace,
];
const result = { ...resultProto };
while (str.length) {
let longestMatch = null;
let finalRule = null;
for (const format of rules) {
const match = str.match(format.regex);
if (match) {
if (!longestMatch || match[0].length > longestMatch[0].length) {
longestMatch = match;
finalRule = format;
}
}
}
if (!finalRule || !longestMatch) {
return false;
}
if (finalRule.callback && finalRule.callback.apply(result, longestMatch) === false) {
return false;
}
str = str.substr(longestMatch[0].length);
finalRule = null;
longestMatch = null;
}
return Math.floor(result.toDate(new Date(nowSeconds * 1000)).getTime() / 1000);
}