read-gedcom
Version:
Gedcom file reader
380 lines • 16.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseExactDate = exports.parseDate = void 0;
const CALENDAR_GREGORIAN = 'DGREGORIAN', CALENDAR_JULIAN = 'DJULIAN', CALENDAR_HEBREW = 'DHEBREW', CALENDAR_FRENCH_REPUBLICAN = 'DFRENCH R', CALENDAR_UNKNOWN = 'DUNKNOWN';
const DATE_PERIOD_FROM = 'FROM', DATE_PERIOD_TO = 'TO';
const DATE_RANGE_BEFORE = 'BEF', DATE_RANGE_AFTER = 'AFT', DATE_RANGE_BETWEEN = 'BET', DATE_RANGE_AND = 'AND';
const DATE_APPROXIMATED_ABOUT = 'ABT', DATE_APPROXIMATED_CALCULATED = 'CAL', DATE_APPROXIMATED_ESTIMATED = 'EST';
const DATE_INT = 'INT';
const createIndices = (array, looseCase = false) => {
// Start with 1 to preserve the month numbers
const entries = array.map((value, i) => [value, i + 1]);
const capitalizeFirst = (v) => v ? v[0].toUpperCase() + v.substring(1).toLowerCase() : v;
const finalEntries = looseCase ? entries.concat(entries.map(([value, n]) => [capitalizeFirst(value), n])) : entries;
return Object.fromEntries(finalEntries);
};
const MONTHS_VALUES = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'];
const MONTHS_FRENCH_VALUES = ['VEND', 'BRUM', 'FRIM', 'NIVO', 'PLUV', 'VENT', 'GERM', 'FLOR', 'PRAI', 'MESS', 'THER', 'FRUC', 'COMP'];
const MONTHS_HEBREW_VALUES = ['TSH', 'CSH', 'KSL', 'TVT', 'SHV', 'ADR', 'ADS', 'NSN', 'IYR', 'SVN', 'TMZ', 'AAV', 'ELL'];
const MONTHS = createIndices(MONTHS_VALUES, true); // Old software format months that way, so we allow them
const MONTHS_FRENCH = createIndices(MONTHS_FRENCH_VALUES);
const MONTHS_HEBREW = createIndices(MONTHS_HEBREW_VALUES);
const BEFORE_COMMON_ERA = createIndices(['BCE', 'BC', 'B.C.']);
const rDateCalendarEscape = /^@#(.*)@$/; // FIXME
const rDatePhrase = /^\((.*)\)$/;
const rDatePhraseEnd = /\((.*)\)$/;
const gYear = '([1-9][0-9]*|0|00[1-9]|0[1-9][0-9])';
const rYear = new RegExp(`^${gYear}$`);
const rYearDual = new RegExp(`^${gYear}/([0-9]{2})$`);
const rDay = /^(?:0?[1-9]|[1-2][0-9]|3[0-1])$/; // Allow leading zeros
const daysInJulianMonth = (month, isBissextile) => month === 2 ? 28 + (isBissextile ? 1 : 0) : 30 + ([4, 6, 9, 11].includes(month) ? 0 : 1);
const isValidPartialDateJulian = (isBissextile, month, day) => {
if (month != null) {
if (month < 1 || month > MONTHS_VALUES.length) { // This check is redundant, but included for completeness
return false;
}
if (day != null) {
const daysInMonth = daysInJulianMonth(month, isBissextile);
if (day < 1 || day > daysInMonth) {
return false;
}
}
}
return true;
};
const isValidDateGregorian = (year, month, day) => {
const isBissextile = ((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0);
return isValidPartialDateJulian(isBissextile, month, day);
};
const isValidDateJulian = (year, month, day) => {
const isBissextile = year % 4 === 0;
return isValidPartialDateJulian(isBissextile, month, day);
};
const isValidDateFrenchRepublican = (year, month, day) => {
if (!(year >= 1 && year <= 14)) {
return false;
}
if (month != null) {
if (month < 1 || month > MONTHS_FRENCH_VALUES.length) {
return false;
}
if (day != null) {
const isBissextile = year % 4 === 3;
const daysInMonth = month !== 13 ? 30 : 5 + (isBissextile ? 1 : 0);
if (day < 1 || day > daysInMonth) {
return false;
}
}
}
return true;
};
const isValidHebrew = (year, month, day) => {
if (year < 1) { // Hebrew calendar is not proleptic
return false;
}
if (month != null) {
if (month < 1 || month > MONTHS_HEBREW_VALUES.length) {
return false;
}
if (day != null) {
const numberOfDays = 29 + ([1, 5, 6, 8, 10, 12].includes(month) ? 1 : 0) + (month === 2 || month === 3 ? 1 : 0); // TODO: this check is not strict enough
if (day < 1 || day > numberOfDays) {
return false;
}
}
}
return true;
};
/**
* Parses a Gedcom date. These dates can take many different forms, see {@link ValueDate}.
* The dates are checked for validity with respect to their calendar, except for the Hebrew calendar which will assume all dates to be valid due to a missing implementation.
* Any unsuccessful parsing or invalid date(s) will result in <code>null</code>.
* @param value The value to parse
* @category Value parsers
*/
const parseDate = (value) => {
if (!value) {
return null;
}
value = value.trim(); // Some files contain leading/trailing spaces
const defaultDateKinds = {
hasDate: true,
hasPhrase: false,
isDatePunctual: false,
isDatePeriod: false,
isDateRange: false,
isDateApproximated: false,
isDateInterpreted: false,
};
let textMatch;
if ((textMatch = rDatePhrase.exec(value)) !== null) {
const text = textMatch[1];
return Object.assign(Object.assign({}, defaultDateKinds), { hasDate: false, hasPhrase: true, phrase: text });
}
const parts = [];
let j = 0;
const spaceChar = ' ', escapeStart = '@#', escapeEndChar = '@';
for (let k = 0; k < value.length; k++) {
if (value.substring(k, k + escapeStart.length) === escapeStart) {
k += escapeStart.length;
while (k < value.length && value[k] !== escapeEndChar) {
k++;
}
}
else {
if (value[k] === spaceChar) {
parts.push(value.substring(j, k));
j = k + 1;
}
}
}
parts.push(value.substring(j, value.length));
let i = 0;
const parseYearPart = (parts, allowBce, allowDual) => {
const defaultYearModifiers = {
isBce: false,
isDual: false,
};
let dual;
if (i < parts.length && rYear.exec(parts[i]) !== null) {
const value = parseInt(parts[i]);
i++;
if (allowBce) {
let isBce = false;
if (i < parts.length && BEFORE_COMMON_ERA[parts[i]] !== undefined) {
isBce = true;
i++;
}
return Object.assign(Object.assign({}, defaultYearModifiers), { value,
isBce });
}
else {
return Object.assign(Object.assign({}, defaultYearModifiers), { value });
}
}
else if (allowDual && i < parts.length && (dual = rYearDual.exec(parts[i])) !== null) {
const value = parseInt(dual[1]), valueDual = parseInt(dual[2]);
i++;
return Object.assign(Object.assign({}, defaultYearModifiers), { value, isDual: true, valueDual });
}
else {
return null; // Underflow or not a year
}
};
const parseDatePart = (parts) => {
var _a;
if (i < parts.length) {
let escapeMatch;
let calendar = CALENDAR_GREGORIAN;
if ((escapeMatch = rDateCalendarEscape.exec(parts[i])) !== null) { // Starts with a calendar
calendar = (_a = escapeMatch[1]) !== null && _a !== void 0 ? _a : escapeMatch[2];
i++;
}
const isGregorian = calendar === CALENDAR_GREGORIAN, isJulian = calendar === CALENDAR_JULIAN, isHebrew = calendar === CALENDAR_HEBREW, isFrenchRepublican = calendar === CALENDAR_FRENCH_REPUBLICAN, isUnknown = calendar === CALENDAR_UNKNOWN;
const isGregorianOrJulian = isGregorian || isJulian;
const calendarProps = {
isGregorian, isJulian, isHebrew, isFrenchRepublican, isUnknown,
};
let monthsIndices;
if (isGregorian || isJulian) {
monthsIndices = MONTHS;
}
else if (isHebrew) {
monthsIndices = MONTHS_HEBREW;
}
else if (isFrenchRepublican) {
monthsIndices = MONTHS_FRENCH;
}
else {
return null; // Unknown/invalid calendar
}
let monthIndex;
if (i < parts.length && (monthIndex = monthsIndices[parts[i]]) !== undefined) { // Format month-year
i++;
const year = parseYearPart(parts, false, isGregorianOrJulian);
if (year !== null) {
if ((isGregorian && !isValidDateGregorian(year.value, monthIndex)) ||
(isJulian && !isValidDateJulian(year.value, monthIndex)) ||
(isFrenchRepublican && !isValidDateFrenchRepublican(year.value, monthIndex)) ||
(isHebrew && !isValidHebrew(year.value, monthIndex))) {
return null;
}
return {
calendar: calendarProps,
month: monthIndex,
year: year,
};
}
else {
return null; // Invalid year
}
}
else {
const previousI = i;
const firstAsYear = parseYearPart(parts, isGregorianOrJulian, false);
if (firstAsYear !== null && firstAsYear.isBce) {
return {
calendar: calendarProps,
year: firstAsYear,
};
}
i = previousI; // Important: we need to backtrack since there can be an ambiguity
const firstAsDay = i < parts.length && rDay.exec(parts[i]) !== null ? parseInt(parts[i]) : null;
let secondAsMonth = null;
if (firstAsDay !== null && i + 1 < parts.length) {
const month = monthsIndices[parts[i + 1]];
if (month !== undefined) {
secondAsMonth = month;
}
}
// Remark that `secondAsMonth !== null` implies `firstAsDay !== null`; but the type checker cannot infer it
if (firstAsDay !== null && secondAsMonth !== null) { // Format day-month-year
i += 2;
const year = parseYearPart(parts, false, isGregorianOrJulian);
if (year !== null) {
if ((isGregorian && !isValidDateGregorian(year.value, secondAsMonth, firstAsDay)) ||
(isJulian && !isValidDateJulian(year.value, secondAsMonth, firstAsDay)) ||
(isFrenchRepublican && !isValidDateFrenchRepublican(year.value, secondAsMonth, firstAsDay)) ||
(isHebrew && !isValidHebrew(year.value, secondAsMonth, firstAsDay))) {
return null;
}
return {
calendar: calendarProps,
day: firstAsDay,
month: secondAsMonth,
year: year,
};
}
else {
return null; // Invalid year (or the format is day-month, which is not supported here)
}
}
else if (firstAsYear !== null) { // Format year
i++;
if ((isFrenchRepublican && !isValidDateFrenchRepublican(firstAsYear.value)) ||
(isHebrew && !isValidHebrew(firstAsYear.value))) {
return null;
}
return {
calendar: calendarProps,
year: firstAsYear,
};
}
else {
return null; // Invalid year (or first token)
}
}
}
else {
return null; // Underflow
}
};
i++;
if (parts.length === 0) {
return null; // Underflow
}
else if (parts[0] === DATE_PERIOD_FROM) {
const date = parseDatePart(parts);
if (date !== null) {
if (i === parts.length) { // Only from
return Object.assign(Object.assign({}, defaultDateKinds), { isDatePeriod: true, dateFrom: date });
}
if (i < parts.length && parts[i] === DATE_PERIOD_TO) {
i++;
const date2 = parseDatePart(parts);
if (date2 !== null && i === parts.length) { // Only from
return Object.assign(Object.assign({}, defaultDateKinds), { isDatePeriod: true, dateFrom: date, dateTo: date2 });
}
}
}
}
else if (parts[0] === DATE_PERIOD_TO) {
const date = parseDatePart(parts);
if (date !== null && i === parts.length) {
return Object.assign(Object.assign({}, defaultDateKinds), { isDatePeriod: true, dateTo: date });
}
}
else if (parts[0] === DATE_RANGE_BEFORE) {
const date = parseDatePart(parts);
if (date !== null && i === parts.length) {
return Object.assign(Object.assign({}, defaultDateKinds), { isDateRange: true, dateBefore: date });
}
}
else if (parts[0] === DATE_RANGE_AFTER) {
const date = parseDatePart(parts);
if (date !== null && i === parts.length) {
return Object.assign(Object.assign({}, defaultDateKinds), { isDateRange: true, dateAfter: date });
}
}
else if (parts[0] === DATE_RANGE_BETWEEN) {
const date1 = parseDatePart(parts);
if (date1 !== null) {
if (parts[i] === DATE_RANGE_AND) {
i++;
const date2 = parseDatePart(parts);
if (date2 !== null && i === parts.length) {
return Object.assign(Object.assign({}, defaultDateKinds), { isDateRange: true, dateAfter: date1, dateBefore: date2 });
}
}
}
}
else if (parts[0] === DATE_APPROXIMATED_ABOUT || parts[0] === DATE_APPROXIMATED_CALCULATED || parts[0] === DATE_APPROXIMATED_ESTIMATED) {
const date = parseDatePart(parts);
if (date !== null && i === parts.length) {
return Object.assign(Object.assign({}, defaultDateKinds), { isDatePunctual: true, isDateApproximated: true, approximationKind: {
isAbout: parts[0] === DATE_APPROXIMATED_ABOUT,
isCalculated: parts[0] === DATE_APPROXIMATED_CALCULATED,
isEstimated: parts[0] === DATE_APPROXIMATED_ESTIMATED,
}, date });
}
}
else if (parts[0] === DATE_INT) {
const date = parseDatePart(parts);
if (date !== null) {
const phraseEnd = rDatePhraseEnd.exec(value);
if (phraseEnd !== null) {
const text = phraseEnd[1];
return Object.assign(Object.assign({}, defaultDateKinds), { hasPhrase: true, isDatePunctual: true, isDateInterpreted: true, date, phrase: text });
}
}
}
else { // Normal date
i--;
const date = parseDatePart(parts);
if (date !== null && i === parts.length) {
return Object.assign(Object.assign({}, defaultDateKinds), { isDatePunctual: true, date });
}
}
return null; // All other invalid cases
};
exports.parseDate = parseDate;
/**
* Parses a date of format day-month-year.
* @param value The value to parse
* @category Value parsers
*/
const parseExactDate = (value) => {
if (!value) {
return null;
}
value = value.trim();
const parts = value.split(' ');
if (parts.length !== 3) { // Must contain three parts: day, month, year
return null;
}
const month = MONTHS[parts[1]];
if (rDay.exec(parts[0]) !== null && month !== undefined && rYear.exec(parts[2]) !== null) {
const day = parseInt(parts[0]);
const year = parseInt(parts[2]);
return {
day,
month,
year,
};
}
else { // Invalid date
return null;
}
};
exports.parseExactDate = parseExactDate;
//# sourceMappingURL=date.js.map