locutus
Version:
Locutus other languages' standard libraries to JavaScript for fun and educational purposes
318 lines (317 loc) • 10.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ParseInLocation = ParseInLocation;
const GO_PARSE_TOKENS = [
'Z07:00',
'-07:00',
'-0700',
'2006',
'PM',
'pm',
'15',
'06',
'01',
'02',
'03',
'04',
'05',
'1',
'2',
'3',
];
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const isTokenAt = (layout, index) => GO_PARSE_TOKENS.find((token) => layout.startsWith(token, index));
const tokenRegex = (token) => {
switch (token) {
case '2006':
return '(\\d{4})';
case '06':
return '(\\d{2})';
case '01':
return '(0[1-9]|1[0-2])';
case '1':
return '([1-9]|1[0-2])';
case '02':
return '(0[1-9]|[12]\\d|3[01])';
case '2':
return '([1-9]|[12]\\d|3[01])';
case '15':
return '([01]\\d|2[0-3])';
case '03':
return '(0[1-9]|1[0-2])';
case '3':
return '([1-9]|1[0-2])';
case '04':
return '([0-5]\\d)';
case '05':
return '([0-5]\\d)';
case 'PM':
return '(AM|PM)';
case 'pm':
return '(am|pm)';
case '-0700':
return '([+-]\\d{4})';
case '-07:00':
return '([+-]\\d{2}:\\d{2})';
case 'Z07:00':
return '(Z|[+-]\\d{2}:\\d{2})';
default:
return escapeRegex(token);
}
};
const parseOffsetMinutes = (offset) => {
if (offset === 'Z') {
return 0;
}
const compact = offset.match(/^([+-])(\d{2})(\d{2})$/);
if (compact?.[1] && compact[2] && compact[3]) {
const sign = compact[1] === '-' ? -1 : 1;
const hours = Number.parseInt(compact[2], 10);
const minutes = Number.parseInt(compact[3], 10);
if (hours > 23 || minutes > 59) {
throw new TypeError('ParseInLocation(): invalid offset');
}
return sign * (hours * 60 + minutes);
}
const colon = offset.match(/^([+-])(\d{2}):(\d{2})$/);
if (colon?.[1] && colon[2] && colon[3]) {
const sign = colon[1] === '-' ? -1 : 1;
const hours = Number.parseInt(colon[2], 10);
const minutes = Number.parseInt(colon[3], 10);
if (hours > 23 || minutes > 59) {
throw new TypeError('ParseInLocation(): invalid offset');
}
return sign * (hours * 60 + minutes);
}
throw new TypeError('ParseInLocation(): invalid offset');
};
const resolveTwoDigitYear = (year) => (year >= 69 ? 1900 + year : 2000 + year);
const createUtcDate = (year, month, day, hour, minute, second) => {
const date = new Date(0);
date.setUTCFullYear(year, month - 1, day);
date.setUTCHours(hour, minute, second, 0);
return date;
};
const parseGoTime = (layout, value) => {
const resolvedLayout = String(layout);
const resolvedValue = String(value);
const tokenOrder = [];
let regexSource = '^';
for (let i = 0; i < resolvedLayout.length;) {
const token = isTokenAt(resolvedLayout, i);
if (token) {
regexSource += tokenRegex(token);
tokenOrder.push(token);
i += token.length;
continue;
}
const char = resolvedLayout[i];
if (char === undefined) {
break;
}
regexSource += escapeRegex(char);
i += 1;
}
regexSource += '$';
const match = new RegExp(regexSource).exec(resolvedValue);
if (!match) {
throw new TypeError('ParseInLocation(): input does not match layout');
}
let year = 1970;
let month = 1;
let day = 1;
let hour24 = 0;
let minute = 0;
let second = 0;
let hour12;
let ampm;
let offsetMinutes = 0;
let hasOffset = false;
let matchIndex = 1;
for (const token of tokenOrder) {
const raw = match[matchIndex];
matchIndex += 1;
if (!raw) {
continue;
}
switch (token) {
case '2006':
year = Number.parseInt(raw, 10);
break;
case '06':
year = resolveTwoDigitYear(Number.parseInt(raw, 10));
break;
case '01':
case '1':
month = Number.parseInt(raw, 10);
break;
case '02':
case '2':
day = Number.parseInt(raw, 10);
break;
case '15':
hour24 = Number.parseInt(raw, 10);
break;
case '03':
case '3':
hour12 = Number.parseInt(raw, 10);
break;
case '04':
minute = Number.parseInt(raw, 10);
break;
case '05':
second = Number.parseInt(raw, 10);
break;
case 'PM':
case 'pm':
ampm = raw.toUpperCase() === 'PM' ? 'PM' : 'AM';
break;
case '-0700':
case '-07:00':
case 'Z07:00':
offsetMinutes = parseOffsetMinutes(raw);
hasOffset = true;
break;
default:
break;
}
}
if (hour12 !== undefined) {
if (ampm === 'PM' && hour12 < 12) {
hour24 = hour12 + 12;
}
else if (ampm === 'AM' && hour12 === 12) {
hour24 = 0;
}
else {
hour24 = hour12;
}
}
if (month < 1 ||
month > 12 ||
day < 1 ||
day > 31 ||
hour24 < 0 ||
hour24 > 23 ||
minute < 0 ||
minute > 59 ||
second < 0 ||
second > 59) {
throw new TypeError('ParseInLocation(): invalid datetime components');
}
const localCheck = createUtcDate(year, month, day, hour24, minute, second);
if (localCheck.getUTCFullYear() !== year ||
localCheck.getUTCMonth() + 1 !== month ||
localCheck.getUTCDate() !== day ||
localCheck.getUTCHours() !== hour24 ||
localCheck.getUTCMinutes() !== minute ||
localCheck.getUTCSeconds() !== second) {
throw new TypeError('ParseInLocation(): invalid datetime components');
}
const localMillis = localCheck.getTime();
return {
localMillis,
hasOffset,
offsetMinutes,
};
};
const zonedFormatterCache = new Map();
const getZonedFormatter = (timeZone) => {
let formatter = zonedFormatterCache.get(timeZone);
if (!formatter) {
formatter = new Intl.DateTimeFormat('en-CA', {
timeZone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hourCycle: 'h23',
});
zonedFormatterCache.set(timeZone, formatter);
}
return formatter;
};
const assertValidLocation = (location) => {
try {
getZonedFormatter(location).format(new Date(0));
}
catch {
throw new TypeError('ParseInLocation(): invalid location');
}
};
const getOffsetMinutesAt = (timeZone, utcMillis) => {
const formatter = getZonedFormatter(timeZone);
const parts = formatter.formatToParts(new Date(utcMillis));
let year = 0;
let month = 1;
let day = 1;
let hour = 0;
let minute = 0;
let second = 0;
for (const part of parts) {
switch (part.type) {
case 'year':
year = Number.parseInt(part.value, 10);
break;
case 'month':
month = Number.parseInt(part.value, 10);
break;
case 'day':
day = Number.parseInt(part.value, 10);
break;
case 'hour':
hour = Number.parseInt(part.value, 10);
break;
case 'minute':
minute = Number.parseInt(part.value, 10);
break;
case 'second':
second = Number.parseInt(part.value, 10);
break;
default:
break;
}
}
const zonedAsUtcMillis = createUtcDate(year, month, day, hour, minute, second).getTime();
return Math.round((zonedAsUtcMillis - utcMillis) / 60_000);
};
const resolveWallTimeInLocation = (timeZone, localMillis) => {
let offsetMinutes = getOffsetMinutesAt(timeZone, localMillis);
if (offsetMinutes !== 0) {
const utcMillis = localMillis - offsetMinutes * 60_000;
const correctedOffsetMinutes = getOffsetMinutesAt(timeZone, utcMillis);
if (correctedOffsetMinutes !== offsetMinutes) {
offsetMinutes = correctedOffsetMinutes;
}
}
return localMillis - offsetMinutes * 60_000;
};
function ParseInLocation(layout, value, location) {
// discuss at: https://locutus.io/golang/time/ParseInLocation
// parity verified: Go 1.23
// original by: Kevin van Zonneveld (https://kvz.io)
// note 1: Supports the same focused layout-token subset as Parse.
// note 2: Inputs without an explicit numeric offset are interpreted in the supplied IANA time zone.
// note 3: Explicit numeric offsets still take precedence over the supplied location, matching Go's absolute-time result.
// example 1: ParseInLocation('2006-01-02 15:04:05', '2026-03-03 13:14:15', 'America/New_York')
// returns 1: new Date('2026-03-03T18:14:15.000Z')
// example 2: ParseInLocation('2006-01-02 15:04:05', '2026-07-03 13:14:15', 'America/New_York')
// returns 2: new Date('2026-07-03T17:14:15.000Z')
// example 3: ParseInLocation('2006-01-02 15:04:05', '2024-11-03 01:30:00', 'America/New_York')
// returns 3: new Date('2024-11-03T05:30:00.000Z')
// example 4: ParseInLocation('2006-01-02 15:04:05', '2024-03-10 02:30:00', 'America/New_York')
// returns 4: new Date('2024-03-10T06:30:00.000Z')
// example 5: ParseInLocation('2006-01-02T15:04:05-07:00', '2026-03-03T13:14:15+02:30', 'America/New_York')
// returns 5: new Date('2026-03-03T10:44:15.000Z')
// example 6: ParseInLocation('2006-01-02 15:04:05', '2026-03-03 13:14:15', ('America' + '/New_York'))
// returns 6: new Date('2026-03-03T18:14:15.000Z')
const resolvedLocation = String(location);
assertValidLocation(resolvedLocation);
const parsed = parseGoTime(layout, value);
const utcMillis = parsed.hasOffset
? parsed.localMillis - parsed.offsetMinutes * 60_000
: resolveWallTimeInLocation(resolvedLocation, parsed.localMillis);
return new Date(utcMillis);
}