From b273f71d60c149cb6a9e8bc1d1029dd2dd805742 Mon Sep 17 00:00:00 2001 From: softsimon Date: Tue, 12 Apr 2022 17:33:44 +0400 Subject: [PATCH] Breaking out strtotime from locutus lib --- backend/package-lock.json | 27 - backend/package.json | 2 - backend/src/api/bisq/markets-api.ts | 12 +- backend/src/api/bisq/strtotime.ts | 1375 +++++++++++++++++++++++++++ 4 files changed, 1381 insertions(+), 35 deletions(-) create mode 100644 backend/src/api/bisq/strtotime.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index be9c47130..33ef22b07 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -15,7 +15,6 @@ "bitcoinjs-lib": "6.0.1", "crypto-js": "^4.0.0", "express": "^4.17.1", - "locutus": "^2.0.12", "mysql2": "2.3.3", "node-worker-threads-pool": "^1.4.3", "socks-proxy-agent": "^6.1.1", @@ -25,7 +24,6 @@ "devDependencies": { "@types/compression": "^1.0.1", "@types/express": "^4.17.2", - "@types/locutus": "^0.0.6", "tslint": "^6.1.0" } }, @@ -114,12 +112,6 @@ "@types/range-parser": "*" } }, - "node_modules/@types/locutus": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/locutus/-/locutus-0.0.6.tgz", - "integrity": "sha512-P+BQds4wrJhqKiIOBWAYpbsE9UOztnnqW9zHk4Bci7kCXjEQAA7FJrD9HX5JU2Z36fhE2WDctuuIpLvqDsciWQ==", - "dev": true - }, "node_modules/@types/mime": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", @@ -804,14 +796,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/locutus": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/locutus/-/locutus-2.0.15.tgz", - "integrity": "sha512-2xWC4RkoAoCVXEb/stzEgG1TNgd+mrkLBj6TuEDNyUoKeQ2XzDTyJUC23sMiqbL6zJmJSP3w59OZo+zc4IBOmA==", - "engines": { - "node": ">= 10" - } - }, "node_modules/long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", @@ -1562,12 +1546,6 @@ "@types/range-parser": "*" } }, - "@types/locutus": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/locutus/-/locutus-0.0.6.tgz", - "integrity": "sha512-P+BQds4wrJhqKiIOBWAYpbsE9UOztnnqW9zHk4Bci7kCXjEQAA7FJrD9HX5JU2Z36fhE2WDctuuIpLvqDsciWQ==", - "dev": true - }, "@types/mime": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", @@ -2135,11 +2113,6 @@ "esprima": "^4.0.0" } }, - "locutus": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/locutus/-/locutus-2.0.15.tgz", - "integrity": "sha512-2xWC4RkoAoCVXEb/stzEgG1TNgd+mrkLBj6TuEDNyUoKeQ2XzDTyJUC23sMiqbL6zJmJSP3w59OZo+zc4IBOmA==" - }, "long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index 472b1af0b..31bec5677 100644 --- a/backend/package.json +++ b/backend/package.json @@ -34,7 +34,6 @@ "bitcoinjs-lib": "6.0.1", "crypto-js": "^4.0.0", "express": "^4.17.1", - "locutus": "^2.0.12", "mysql2": "2.3.3", "node-worker-threads-pool": "^1.4.3", "socks-proxy-agent": "^6.1.1", @@ -44,7 +43,6 @@ "devDependencies": { "@types/compression": "^1.0.1", "@types/express": "^4.17.2", - "@types/locutus": "^0.0.6", "tslint": "^6.1.0" } } diff --git a/backend/src/api/bisq/markets-api.ts b/backend/src/api/bisq/markets-api.ts index 558c390a0..54e0297b7 100644 --- a/backend/src/api/bisq/markets-api.ts +++ b/backend/src/api/bisq/markets-api.ts @@ -1,7 +1,7 @@ import { Currencies, OffersData, TradesData, Depth, Currency, Interval, HighLowOpenClose, Markets, Offers, Offer, BisqTrade, MarketVolume, Tickers, Ticker, SummarizedIntervals, SummarizedInterval } from './interfaces'; -import * as datetime from 'locutus/php/datetime'; +const strtotime = require('./strtotime'); class BisqMarketsApi { private cryptoCurrencyData: Currency[] = []; @@ -312,7 +312,7 @@ class BisqMarketsApi { getTickerFromMarket(market: string): Ticker | null { let ticker: Ticker; - const timestamp_from = datetime.strtotime('-24 hour'); + const timestamp_from = strtotime('-24 hour'); const timestamp_to = new Date().getTime() / 1000; const trades = this.getTradesByCriteria(market, timestamp_to, timestamp_from, undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER); @@ -638,13 +638,13 @@ class BisqMarketsApi { case 'half_day': return (ts - (ts % (3600 * 12))); case 'day': - return datetime.strtotime('midnight today', ts); + return strtotime('midnight today', ts); case 'week': - return datetime.strtotime('midnight sunday last week', ts); + return strtotime('midnight sunday last week', ts); case 'month': - return datetime.strtotime('midnight first day of this month', ts); + return strtotime('midnight first day of this month', ts); case 'year': - return datetime.strtotime('midnight first day of january', ts); + return strtotime('midnight first day of january', ts); default: throw new Error('Unsupported interval: ' + interval); } diff --git a/backend/src/api/bisq/strtotime.ts b/backend/src/api/bisq/strtotime.ts new file mode 100644 index 000000000..912f00ec9 --- /dev/null +++ b/backend/src/api/bisq/strtotime.ts @@ -0,0 +1,1375 @@ +// @ts-nocheck + +/* +Copyright (c) 2007-2016 Kevin van Zonneveld (https://kvz.io) +and Contributors (https://locutus.io/authors) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/* + +https://github.com/locutusjs/locutus/blob/master/src/php/datetime/strtotime.js + +*/ + +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) { + meridian = meridian && meridian.toLowerCase() + + switch (meridian) { + 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) { + return { + 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 + }[monthStr.toLowerCase()] +} + +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], + behavior: relativeBehavior[relTextLower] || 0 + } +} + +function processTzCorrection (tzOffset, oldValue) { + const reTzCorrectionLoose = /(?:GMT)?([+-])(\d+)(:?)(\d{0,2})/i + tzOffset = tzOffset && tzOffset.match(reTzCorrectionLoose) + + if (!tzOffset) { + return oldValue + } + + const sign = tzOffset[1] === '-' ? -1 : 1 + let hours = +tzOffset[2] + let minutes = +tzOffset[4] + + if (!tzOffset[4] && !tzOffset[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: 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: RegExp('^(' + reReltextnumber + '|' + reReltexttext + ')' + reSpace + '(' + reDayfull + '|' + reDayabbr + ')' + reSpace + 'of', 'i'), + name: 'weekdayof' + // todo + }, + + mssqltime: { + regex: 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)) + } + }, + + timeLong12: { + regex: 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: RegExp('^' + reHour12 + '[:.]' + reMinutelz + reSpaceOpt + reMeridian, 'i'), + name: 'timeshort12', + callback (match, hour, minute, meridian) { + return this.time(processMeridian(+hour, meridian), +minute, 0, 0) + } + }, + + timeTiny12: { + regex: RegExp('^' + reHour12 + reSpaceOpt + reMeridian, 'i'), + name: 'timetiny12', + callback (match, hour, meridian) { + return this.time(processMeridian(+hour, meridian), 0, 0, 0) + } + }, + + soap: { + regex: 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: 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: 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: 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: 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: 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: 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: 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: RegExp('^' + reDay + '[.\\t-]' + reMonth + '[.-]' + reYear4), + name: 'pointeddate4', + callback (match, day, month, year) { + return this.ymd(+year, month - 1, +day) + } + }, + + pointedDate2: { + regex: RegExp('^' + reDay + '[.\\t]' + reMonth + '\\.' + reYear2), + name: 'pointeddate2', + callback (match, day, month, year) { + return this.ymd(processYear(year), month - 1, +day) + } + }, + + timeLong24: { + regex: RegExp('^t?' + reHour24 + '[:.]' + reMinute + '[:.]' + reSecond), + name: 'timelong24', + callback (match, hour, minute, second) { + return this.time(+hour, +minute, +second, 0) + } + }, + + dateNoColon: { + regex: RegExp('^' + reYear4 + reMonthlz + reDaylz), + name: 'datenocolon', + callback (match, year, month, day) { + return this.ymd(+year, month - 1, +day) + } + }, + + pgydotd: { + regex: RegExp('^' + reYear4 + '\\.?' + reDayOfYear), + name: 'pgydotd', + callback (match, year, day) { + return this.ymd(+year, 0, +day) + } + }, + + timeShort24: { + regex: RegExp('^t?' + reHour24 + '[:.]' + reMinute, 'i'), + name: 'timeshort24', + callback (match, hour, minute) { + return this.time(+hour, +minute, 0, 0) + } + }, + + iso8601noColon: { + regex: 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: RegExp('^' + reYear4 + '/' + reMonthlz + '/' + reDaylz + '/'), + name: 'iso8601dateslash', + callback (match, year, month, day) { + return this.ymd(+year, month - 1, +day) + } + }, + + dateSlash: { + regex: RegExp('^' + reYear4 + '/' + reMonth + '/' + reDay), + name: 'dateslash', + callback (match, year, month, day) { + return this.ymd(+year, month - 1, +day) + } + }, + + american: { + regex: RegExp('^' + reMonth + '/' + reDay + '/' + reYear), + name: 'american', + callback (match, month, day, year) { + return this.ymd(processYear(year), month - 1, +day) + } + }, + + americanShort: { + regex: 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: RegExp('^' + reYear + '-' + reMonth + '-' + reDay), + name: 'gnudateshort | iso8601date2', + callback (match, year, month, day) { + return this.ymd(processYear(year), month - 1, +day) + } + }, + + iso8601date4: { + regex: RegExp('^' + reYear4withSign + '-' + reMonthlz + '-' + reDaylz), + name: 'iso8601date4', + callback (match, year, month, day) { + return this.ymd(+year, month - 1, +day) + } + }, + + gnuNoColon: { + regex: 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: 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: 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: RegExp('^' + reDay + '[ \\t.-]*' + reMonthText + '[ \\t.-]*' + reYear, 'i'), + name: 'datefull', + callback (match, day, month, year) { + return this.ymd(processYear(year), lookupMonth(month), +day) + } + }, + + dateNoDay: { + regex: RegExp('^' + reMonthText + '[ .\\t-]*' + reYear4, 'i'), + name: 'datenoday', + callback (match, month, year) { + return this.ymd(+year, lookupMonth(month), 1) + } + }, + + dateNoDayRev: { + regex: RegExp('^' + reYear4 + '[ .\\t-]*' + reMonthText, 'i'), + name: 'datenodayrev', + callback (match, year, month) { + return this.ymd(+year, lookupMonth(month), 1) + } + }, + + pgTextShort: { + regex: RegExp('^(' + reMonthAbbr + ')-' + reDaylz + '-' + reYear, 'i'), + name: 'pgtextshort', + callback (match, month, day, year) { + return this.ymd(processYear(year), lookupMonth(month), +day) + } + }, + + dateNoYear: { + regex: RegExp('^' + reDateNoYear, 'i'), + name: 'datenoyear', + callback (match, month, day) { + return this.ymd(this.y, lookupMonth(month), +day) + } + }, + + dateNoYearRev: { + regex: RegExp('^' + reDay + '[ .\\t-]*' + reMonthText, 'i'), + name: 'datenoyearrev', + callback (match, day, month) { + return this.ymd(this.y, lookupMonth(month), +day) + } + }, + + isoWeekDay: { + regex: RegExp('^' + reYear4 + '-?W' + reWeekOfYear + '(?:-?([0-7]))?'), + name: 'isoweekday | isoweek', + callback (match, year, week, day) { + day = day ? +day : 1 + + if (!this.ymd(+year, 0, 1)) { + return false + } + + // get day of week for Jan 1st + let dayOfWeek = new Date(this.y, this.m, this.d).getDay() + + // and use the day to figure out the offset for day 1 of week 1 + dayOfWeek = 0 - (dayOfWeek > 4 ? dayOfWeek - 7 : dayOfWeek) + + this.rd += dayOfWeek + ((week - 1) * 7) + day + } + }, + + relativeText: { + regex: RegExp('^(' + reReltextnumber + '|' + reReltexttext + ')' + reSpace + '(' + reReltextunit + ')', 'i'), + name: 'relativetext', + callback (match, relValue, relUnit) { + // todo: implement handling of 'this time-unit' + // eslint-disable-next-line no-unused-vars + 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: 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: RegExp('^(' + reDaytext + ')', 'i'), + name: 'daytext', + callback (match, dayText) { + this.resetTime() + this.weekday = lookupWeekday(dayText, 0) + + if (this.weekdayBehavior !== 2) { + this.weekdayBehavior = 1 + } + } + }, + + relativeTextWeek: { + regex: 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: RegExp('^(' + reMonthFull + '|' + reMonthAbbr + ')', 'i'), + name: 'monthfull | monthabbr', + callback (match, month) { + return this.ymd(this.y, lookupMonth(month), this.d) + } + }, + + tzCorrection: { + regex: RegExp('^' + reTzCorrection, 'i'), + name: 'tzcorrection', + callback (tzCorrection) { + return this.zone(processTzCorrection(tzCorrection)) + } + }, + + tzAbbr: { + regex: RegExp('^' + reTzAbbr), + name: 'tzabbr', + callback (match, abbr) { + const offset = tzAbbrOffsets[abbr.toLowerCase()] + + if (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: RegExp('^' + reYear4), + name: 'year4', + callback (match, year) { + this.y = +year + return true + } + }, + + whitespace: { + regex: /^[ .,\t]+/, + name: 'whitespace' + // do nothing + }, + + dateShortWithTimeLong: { + regex: 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: 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: 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: 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 + } +} + +module.exports = 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 + + if (now == null) { + now = Math.floor(Date.now() / 1000) + } + + // 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.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 = Object.create(resultProto) + + while (str.length) { + let longestMatch = null + let finalRule = null + + for (let i = 0, l = rules.length; i < l; i++) { + const format = rules[i] + + const match = str.match(format.regex) + + if (match) { + if (!longestMatch || match[0].length > longestMatch[0].length) { + longestMatch = match + finalRule = format + } + } + } + + if (!finalRule || (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(now * 1000)) / 1000) +} \ No newline at end of file