UNPKG

read-gedcom

Version:
380 lines 16.4 kB
"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