mirror of
https://github.com/PeWu/topola-viewer.git
synced 2025-12-23 18:50:04 +00:00
Implement age calculation for death event (#80)
This commit is contained in:
parent
6b030a1ccc
commit
0b8084e3bc
@ -1,5 +1,6 @@
|
||||
import flatMap from 'array.prototype.flatmap';
|
||||
import {compareDates, translateDate} from '../util/date_util';
|
||||
import {calcAge} from '../util/age_util';
|
||||
import {DateOrRange, getDate} from 'topola';
|
||||
import {dereference, GedcomData, getData} from '../util/gedcom_util';
|
||||
import {GedcomEntry} from 'parse-gedcom';
|
||||
@ -80,6 +81,31 @@ function eventFamilyDetails(
|
||||
return null;
|
||||
}
|
||||
|
||||
function eventAdditionalDetails(
|
||||
eventEntry: GedcomEntry,
|
||||
indi: string,
|
||||
gedcom: GedcomData,
|
||||
intl: IntlShape,
|
||||
) {
|
||||
if (eventEntry.tag === 'DEAT') {
|
||||
const deathDate = resolveDate(eventEntry);
|
||||
|
||||
const birthDate = gedcom.indis[indi].tree
|
||||
.filter((indiSubEntry) => indiSubEntry.tag === 'BIRT')
|
||||
.map((birthEvent) => resolveDate(birthEvent))
|
||||
.find((topolaDate) => topolaDate);
|
||||
|
||||
if (birthDate && deathDate) {
|
||||
return (
|
||||
<div className="meta">
|
||||
{calcAge(birthDate?.data, deathDate?.data, intl)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function eventPlace(entry: GedcomEntry) {
|
||||
const place = entry.tree.find((subEntry) => subEntry.tag === 'PLAC');
|
||||
if (place && place.data) {
|
||||
@ -151,7 +177,7 @@ function toIndiEvent(
|
||||
date: date ? getDate(date.data) : undefined,
|
||||
type: entry.tag,
|
||||
header: eventHeader(entry.tag, date, intl),
|
||||
subHeader: null,
|
||||
subHeader: eventAdditionalDetails(entry, indi, gedcom, intl),
|
||||
place: eventPlace(entry),
|
||||
notes: eventNotes(entry, gedcom),
|
||||
},
|
||||
|
||||
@ -65,6 +65,10 @@
|
||||
"date.after": "po {from}",
|
||||
"date.before": "przed {to}",
|
||||
"error.error": "Błąd",
|
||||
"age.exact": "{age, plural, =0 {Mniej niż 1 rok} one {{qualifier} 1 rok} many {{qualifier} # lat} other {{qualifier} # lata}}",
|
||||
"age.less": "Mniej niż {age, plural, =0 {1 rok} one {1 rok} many {# lat} other {# lata}}",
|
||||
"age.more": "Więcej niż {age, plural, =0 {0 lat} one {1 rok} many {# lat} other {# lata}}",
|
||||
"age.between": "Między {ageFrom} a {ageTo, plural, =0 {0 lat} one {1 rok} many {# lat} other {# lata}}",
|
||||
"error.failed_pdf": "Nie udało się utworzyć pliku PDF. Spróbuj jeszcze raz z mniejszym diagramem lub pobierz plik SVG.",
|
||||
"error.failed_png": "Nie udało się utworzyć pliku PNG. Spróbuj jeszcze raz z mniejszym diagramem lub pobierz plik SVG.",
|
||||
"error.failed_to_load_file": "Błąd wczytywania pliku",
|
||||
|
||||
120
src/util/age_util.spec.ts
Normal file
120
src/util/age_util.spec.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import expect from 'expect';
|
||||
import {createIntl} from 'react-intl';
|
||||
import {calcAge} from './age_util';
|
||||
|
||||
const intl = createIntl({
|
||||
locale: 'en',
|
||||
messages: {},
|
||||
});
|
||||
|
||||
describe('calcAge()', () => {
|
||||
it('age 1 year', () => {
|
||||
const age = calcAge('1999', '2000', intl);
|
||||
expect(age).toEqual('1 year');
|
||||
});
|
||||
it('age multiple years', () => {
|
||||
const age = calcAge('1999', '2003', intl);
|
||||
expect(age).toEqual('4 years');
|
||||
});
|
||||
it('0 years as Less than 1 year', () => {
|
||||
const age = calcAge('1 Sep 1990', '1 Oct 1990', intl);
|
||||
expect(age).toEqual('Less than 1 year');
|
||||
});
|
||||
it('age with qualifier', () => {
|
||||
const age = calcAge('ABT 1990', '2021', intl);
|
||||
expect(age).toEqual('about 31 years');
|
||||
});
|
||||
it('age for full dates', () => {
|
||||
const age = calcAge('1 Sep 1990', '1 Sep 2021', intl);
|
||||
expect(age).toEqual('31 years');
|
||||
});
|
||||
it('age with round down respecting leap years', () => {
|
||||
const age = calcAge('2 Sep 1990', '1 Sep 2021', intl);
|
||||
expect(age).toEqual('30 years');
|
||||
});
|
||||
|
||||
it('age with exact and range between', () => {
|
||||
const age = calcAge('1990', 'BET 2020 AND 2021', intl);
|
||||
expect(age).toEqual('Between 30 and 31 years');
|
||||
});
|
||||
it('age with exact and range after', () => {
|
||||
const age = calcAge('1990', 'AFT 2021', intl);
|
||||
expect(age).toEqual('More than 31 years');
|
||||
});
|
||||
it('age with exact and range before', () => {
|
||||
const age = calcAge('1990', 'BEF 2021', intl);
|
||||
expect(age).toEqual('Less than 31 years');
|
||||
});
|
||||
|
||||
it('age with range between and exact', () => {
|
||||
const age = calcAge('BET 1990 AND 1991', '2021', intl);
|
||||
expect(age).toEqual('Between 30 and 31 years');
|
||||
});
|
||||
it('age with 2 ranges between', () => {
|
||||
const age = calcAge('BET 1990 AND 1991', 'BET 2020 AND 2021', intl);
|
||||
expect(age).toEqual('Between 29 and 31 years');
|
||||
});
|
||||
it('age with range between and range after', () => {
|
||||
const age = calcAge('BET 1990 AND 1991', 'AFT 2021', intl);
|
||||
expect(age).toEqual('More than 30 years');
|
||||
});
|
||||
it('age with range between and range before', () => {
|
||||
const age = calcAge('BET 1990 AND 1991', 'BEF 2021', intl);
|
||||
expect(age).toEqual('Less than 31 years');
|
||||
});
|
||||
|
||||
it('age with range after and exact', () => {
|
||||
const age = calcAge('AFT 1990', '2021', intl);
|
||||
expect(age).toEqual('Less than 31 years');
|
||||
});
|
||||
it('age with range after and range between', () => {
|
||||
const age = calcAge('AFT 1990', 'BET 2020 AND 2021', intl);
|
||||
expect(age).toEqual('Less than 31 years');
|
||||
});
|
||||
it('age with range after and before', () => {
|
||||
const age = calcAge('AFT 1990', 'BEF 2021', intl);
|
||||
expect(age).toEqual('Less than 31 years');
|
||||
});
|
||||
it('age with 2 ranges after cannot be calculated', () => {
|
||||
const age = calcAge('AFT 1990', 'AFT 2021', intl);
|
||||
expect(age).toBeUndefined();
|
||||
});
|
||||
|
||||
it('age with range before and exact', () => {
|
||||
const age = calcAge('BEF 1990', '2021', intl);
|
||||
expect(age).toEqual('More than 31 years');
|
||||
});
|
||||
it('age with ranges before and between', () => {
|
||||
const age = calcAge('BEF 1990', 'BET 2020 AND 2021', intl);
|
||||
expect(age).toEqual('More than 30 years');
|
||||
});
|
||||
it('age with ranges before and after', () => {
|
||||
const age = calcAge('BEF 1990', 'AFT 2021', intl);
|
||||
expect(age).toEqual('More than 31 years');
|
||||
});
|
||||
it('age with 2 ranges before cannot be calculated', () => {
|
||||
const age = calcAge('BEF 1990', 'BEF 2021', intl);
|
||||
expect(age).toBeUndefined();
|
||||
});
|
||||
|
||||
it('age with death before birth cannot be calculated', () => {
|
||||
const age = calcAge('2021', '1990', intl);
|
||||
expect(age).toBeUndefined();
|
||||
});
|
||||
it('age with overlapping between ranges cannot be calculated', () => {
|
||||
const age = calcAge('BET 1990 AND 2000', 'BET 1999 AND 2021 ', intl);
|
||||
expect(age).toBeUndefined();
|
||||
});
|
||||
it('age with invalid between range cannot be calculated', () => {
|
||||
const age = calcAge('BET 1999 AND 1990', ' 2021 ', intl);
|
||||
expect(age).toBeUndefined();
|
||||
});
|
||||
it('age without birth cannot be calculated', () => {
|
||||
const age = calcAge('', '2021', intl);
|
||||
expect(age).toBeUndefined();
|
||||
});
|
||||
it('age without death cannot be calculated', () => {
|
||||
const age = calcAge('1990', '', intl);
|
||||
expect(age).toBeUndefined();
|
||||
});
|
||||
});
|
||||
242
src/util/age_util.ts
Normal file
242
src/util/age_util.ts
Normal file
@ -0,0 +1,242 @@
|
||||
import {Date as TopolaDate} from 'topola/dist/data';
|
||||
import {IntlShape} from 'react-intl';
|
||||
import {DateOrRange, getDate} from 'topola';
|
||||
import {
|
||||
areDateRangesOverlapped,
|
||||
compareDates,
|
||||
formatDateQualifier,
|
||||
isDateRangeClosed,
|
||||
isValidDateOrRange,
|
||||
toDateObject,
|
||||
} from './date_util';
|
||||
|
||||
function formatExactAge(
|
||||
birthDate: TopolaDate,
|
||||
deathDate: TopolaDate,
|
||||
intl: IntlShape,
|
||||
): string {
|
||||
const ageInYears = calcDateDifferenceInYears(birthDate, deathDate);
|
||||
const qualifier = birthDate.qualifier || deathDate.qualifier;
|
||||
const translatedQualifier =
|
||||
qualifier && formatDateQualifier(qualifier, intl) + ' ';
|
||||
|
||||
return intl.formatMessage(
|
||||
{
|
||||
id: 'age.exact',
|
||||
defaultMessage:
|
||||
'{qualifier}{age, plural, =0 {Less than 1 year} one {1 year} other {# years}}',
|
||||
},
|
||||
{age: ageInYears, qualifier: translatedQualifier},
|
||||
);
|
||||
}
|
||||
|
||||
function formatAgeMoreThan(
|
||||
birthDate: TopolaDate,
|
||||
deathDate: TopolaDate,
|
||||
intl: IntlShape,
|
||||
): string {
|
||||
const ageInYears = calcDateDifferenceInYears(birthDate, deathDate);
|
||||
return intl.formatMessage(
|
||||
{
|
||||
id: 'age.more',
|
||||
defaultMessage:
|
||||
'More than {age, plural, =0 {0 years} one {1 year} other {# years}}',
|
||||
},
|
||||
{age: ageInYears},
|
||||
);
|
||||
}
|
||||
|
||||
function formatAgeLessThan(
|
||||
birthDate: TopolaDate,
|
||||
deathDate: TopolaDate,
|
||||
intl: IntlShape,
|
||||
): string {
|
||||
const ageInYears = calcDateDifferenceInYears(birthDate, deathDate);
|
||||
return intl.formatMessage(
|
||||
{
|
||||
id: 'age.less',
|
||||
defaultMessage:
|
||||
'Less than {age, plural, =0 {1 year} one {1 year} other {# years}}',
|
||||
},
|
||||
{age: ageInYears},
|
||||
);
|
||||
}
|
||||
|
||||
function formatAgeBetween(
|
||||
birthDateFrom: TopolaDate,
|
||||
birthDateTo: TopolaDate,
|
||||
deathDateFrom: TopolaDate,
|
||||
deathDateTo: TopolaDate,
|
||||
intl: IntlShape,
|
||||
): string {
|
||||
const ageInYearsFrom = calcDateDifferenceInYears(birthDateTo, deathDateFrom);
|
||||
const ageInYearsTo = calcDateDifferenceInYears(birthDateFrom, deathDateTo);
|
||||
return intl.formatMessage(
|
||||
{
|
||||
id: 'age.between',
|
||||
defaultMessage:
|
||||
'Between {ageFrom} and {ageTo, plural, =0 {0 years} one {1 year} other {# years}}',
|
||||
},
|
||||
{ageFrom: ageInYearsFrom, ageTo: ageInYearsTo},
|
||||
);
|
||||
}
|
||||
|
||||
function canCalculateAge(
|
||||
birthDate: DateOrRange | undefined,
|
||||
deathDate: DateOrRange | undefined,
|
||||
): boolean {
|
||||
if (birthDate && deathDate) {
|
||||
// cannot calculate if there is no valid birth or death date
|
||||
if (!isValidDateOrRange(birthDate) || !isValidDateOrRange(deathDate)) {
|
||||
return false;
|
||||
}
|
||||
//cannot calculate if death date is before birth date
|
||||
if (compareDates(birthDate, deathDate) > 0) {
|
||||
return false;
|
||||
}
|
||||
// cannot calculate if closed date range for birth or death are overlapping
|
||||
if (
|
||||
birthDate.dateRange &&
|
||||
deathDate.dateRange &&
|
||||
isDateRangeClosed(birthDate?.dateRange) &&
|
||||
isDateRangeClosed(deathDate?.dateRange)
|
||||
) {
|
||||
return !areDateRangesOverlapped(birthDate.dateRange, deathDate.dateRange);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function calcDateDifferenceInYears(
|
||||
firstDate: TopolaDate,
|
||||
secondDate: TopolaDate,
|
||||
): number {
|
||||
const firstDateObject = toDateObject(firstDate);
|
||||
const secondDateObject = toDateObject(secondDate);
|
||||
|
||||
const dateDiff = new Date(
|
||||
secondDateObject.valueOf() - firstDateObject.valueOf(),
|
||||
);
|
||||
return Math.abs(dateDiff.getUTCFullYear() - 1970);
|
||||
}
|
||||
|
||||
export function calcAge(
|
||||
birthGedcomDate: string | undefined,
|
||||
deathGedcomDate: string | undefined,
|
||||
intl: IntlShape,
|
||||
): string | undefined {
|
||||
if (birthGedcomDate && deathGedcomDate) {
|
||||
const birthDateOrRange = getDate(birthGedcomDate);
|
||||
const deathDateOrRange = getDate(deathGedcomDate);
|
||||
if (canCalculateAge(birthDateOrRange, deathDateOrRange)) {
|
||||
if (birthDateOrRange?.date) {
|
||||
if (deathDateOrRange?.date) {
|
||||
return formatExactAge(
|
||||
birthDateOrRange.date,
|
||||
deathDateOrRange.date,
|
||||
intl,
|
||||
);
|
||||
}
|
||||
if (
|
||||
deathDateOrRange?.dateRange?.from &&
|
||||
deathDateOrRange.dateRange?.to
|
||||
) {
|
||||
return formatAgeBetween(
|
||||
birthDateOrRange.date,
|
||||
birthDateOrRange.date,
|
||||
deathDateOrRange?.dateRange?.from,
|
||||
deathDateOrRange?.dateRange?.to,
|
||||
intl,
|
||||
);
|
||||
}
|
||||
if (deathDateOrRange?.dateRange?.from) {
|
||||
return formatAgeMoreThan(
|
||||
birthDateOrRange.date,
|
||||
deathDateOrRange.dateRange?.from,
|
||||
intl,
|
||||
);
|
||||
}
|
||||
if (deathDateOrRange?.dateRange?.to) {
|
||||
return formatAgeLessThan(
|
||||
birthDateOrRange.date,
|
||||
deathDateOrRange.dateRange?.to,
|
||||
intl,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (
|
||||
birthDateOrRange?.dateRange?.from &&
|
||||
birthDateOrRange?.dateRange?.to
|
||||
) {
|
||||
if (deathDateOrRange?.date) {
|
||||
return formatAgeBetween(
|
||||
birthDateOrRange?.dateRange?.from,
|
||||
birthDateOrRange?.dateRange?.to,
|
||||
deathDateOrRange?.date,
|
||||
deathDateOrRange?.date,
|
||||
intl,
|
||||
);
|
||||
}
|
||||
if (
|
||||
deathDateOrRange?.dateRange?.from &&
|
||||
deathDateOrRange.dateRange?.to
|
||||
) {
|
||||
return formatAgeBetween(
|
||||
birthDateOrRange?.dateRange?.from,
|
||||
birthDateOrRange?.dateRange?.to,
|
||||
deathDateOrRange?.dateRange?.from,
|
||||
deathDateOrRange?.dateRange?.to,
|
||||
intl,
|
||||
);
|
||||
}
|
||||
if (deathDateOrRange?.dateRange?.from) {
|
||||
return formatAgeMoreThan(
|
||||
birthDateOrRange.dateRange?.to,
|
||||
deathDateOrRange.dateRange?.from,
|
||||
intl,
|
||||
);
|
||||
}
|
||||
if (deathDateOrRange?.dateRange?.to) {
|
||||
return formatAgeLessThan(
|
||||
birthDateOrRange.dateRange?.from,
|
||||
deathDateOrRange.dateRange?.to,
|
||||
intl,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (birthDateOrRange?.dateRange?.from) {
|
||||
if (deathDateOrRange?.date) {
|
||||
return formatAgeLessThan(
|
||||
birthDateOrRange.dateRange?.from,
|
||||
deathDateOrRange.date,
|
||||
intl,
|
||||
);
|
||||
}
|
||||
if (deathDateOrRange?.dateRange?.to) {
|
||||
return formatAgeLessThan(
|
||||
birthDateOrRange.dateRange?.from,
|
||||
deathDateOrRange.dateRange?.to,
|
||||
intl,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (birthDateOrRange?.dateRange?.to) {
|
||||
if (deathDateOrRange?.date) {
|
||||
return formatAgeMoreThan(
|
||||
birthDateOrRange?.dateRange?.to,
|
||||
deathDateOrRange.date,
|
||||
intl,
|
||||
);
|
||||
}
|
||||
if (deathDateOrRange?.dateRange?.from) {
|
||||
return formatAgeMoreThan(
|
||||
birthDateOrRange?.dateRange?.to,
|
||||
deathDateOrRange.dateRange?.from,
|
||||
intl,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -14,19 +14,8 @@ function formatDate(date: TopolaDate, intl: IntlShape) {
|
||||
if (!hasDay && !hasMonth && !hasYear) {
|
||||
return date.text || '';
|
||||
}
|
||||
const dateObject = new Date(
|
||||
hasYear ? date.year! : 0,
|
||||
hasMonth ? date.month! - 1 : 0,
|
||||
hasDay ? date.day! : 1,
|
||||
);
|
||||
|
||||
const qualifier = date.qualifier && date.qualifier.toLowerCase();
|
||||
const translatedQualifier =
|
||||
qualifier &&
|
||||
intl.formatMessage({
|
||||
id: `date.${qualifier}`,
|
||||
defaultMessage: DATE_QUALIFIERS.get(qualifier) || qualifier,
|
||||
});
|
||||
const dateObject = toDateObject(date);
|
||||
const translatedQualifier = formatDateQualifier(date.qualifier, intl);
|
||||
|
||||
const formatOptions: Intl.DateTimeFormatOptions = {
|
||||
day: hasDay ? 'numeric' : undefined,
|
||||
@ -76,6 +65,22 @@ function formatDateRage(dateRange: DateRange, intl: IntlShape) {
|
||||
return '';
|
||||
}
|
||||
|
||||
export function formatDateQualifier(
|
||||
qualifier: string | undefined,
|
||||
intl: IntlShape,
|
||||
): string {
|
||||
const lowerCaseQualifier = qualifier && qualifier.toLowerCase();
|
||||
return (
|
||||
(lowerCaseQualifier &&
|
||||
intl.formatMessage({
|
||||
id: `date.${lowerCaseQualifier}`,
|
||||
defaultMessage:
|
||||
DATE_QUALIFIERS.get(lowerCaseQualifier) || lowerCaseQualifier,
|
||||
})) ||
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
/** Formats a DateOrRange object. */
|
||||
export function formatDateOrRange(
|
||||
dateOrRange: DateOrRange | undefined,
|
||||
@ -98,19 +103,10 @@ export function translateDate(gedcomDate: string, intl: IntlShape): string {
|
||||
return formatDateOrRange(getDate(gedcomDate), intl);
|
||||
}
|
||||
|
||||
/** Compares a dates given in GEDCOM format. */
|
||||
export function compareDates(
|
||||
firstDateOrRange: DateOrRange | undefined,
|
||||
secondDateOrRange: DateOrRange | undefined,
|
||||
export function compareTopolaDates(
|
||||
date1: TopolaDate | undefined,
|
||||
date2: TopolaDate | undefined,
|
||||
): number {
|
||||
const date1 =
|
||||
firstDateOrRange &&
|
||||
(firstDateOrRange.date ||
|
||||
(firstDateOrRange.dateRange && firstDateOrRange.dateRange.from));
|
||||
const date2 =
|
||||
secondDateOrRange &&
|
||||
(secondDateOrRange.date ||
|
||||
(secondDateOrRange.dateRange && secondDateOrRange.dateRange.from));
|
||||
if (!date1 || !date1.year || !date2 || !date2.year) {
|
||||
return 0;
|
||||
}
|
||||
@ -128,3 +124,63 @@ export function compareDates(
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Compares a dates given in GEDCOM format. */
|
||||
export function compareDates(
|
||||
firstDateOrRange: DateOrRange | undefined,
|
||||
secondDateOrRange: DateOrRange | undefined,
|
||||
): number {
|
||||
const date1 =
|
||||
firstDateOrRange &&
|
||||
(firstDateOrRange.date ||
|
||||
(firstDateOrRange.dateRange &&
|
||||
(firstDateOrRange.dateRange.from || firstDateOrRange.dateRange.to)));
|
||||
const date2 =
|
||||
secondDateOrRange &&
|
||||
(secondDateOrRange.date ||
|
||||
(secondDateOrRange.dateRange &&
|
||||
(secondDateOrRange.dateRange.from || secondDateOrRange.dateRange.to)));
|
||||
return compareTopolaDates(date1, date2);
|
||||
}
|
||||
|
||||
export function areDateRangesOverlapped(
|
||||
range1: DateRange,
|
||||
range2: DateRange,
|
||||
): boolean {
|
||||
return (
|
||||
compareTopolaDates(range1.from, range2.to) <= 0 &&
|
||||
compareTopolaDates(range1.to, range2.from) >= 0
|
||||
);
|
||||
}
|
||||
|
||||
export function isValidDateOrRange(
|
||||
dateOrRange: DateOrRange | undefined,
|
||||
): boolean {
|
||||
// invalid when range is closed and start is before end
|
||||
if (isDateRangeClosed(dateOrRange?.dateRange)) {
|
||||
return (
|
||||
compareTopolaDates(
|
||||
dateOrRange?.dateRange?.from,
|
||||
dateOrRange?.dateRange?.to,
|
||||
) <= 0
|
||||
);
|
||||
}
|
||||
//valid when there is exact date or date range has start or end defined
|
||||
return !!(
|
||||
dateOrRange?.date ||
|
||||
dateOrRange?.dateRange?.from ||
|
||||
dateOrRange?.dateRange?.to
|
||||
);
|
||||
}
|
||||
|
||||
export function isDateRangeClosed(range: DateRange | undefined): boolean {
|
||||
return !!(range?.from && range?.to);
|
||||
}
|
||||
|
||||
export function toDateObject(date: TopolaDate): Date {
|
||||
return new Date(
|
||||
date.year !== undefined ? date.year! : 0,
|
||||
date.month !== undefined ? date.month! - 1 : 0,
|
||||
date.day !== undefined ? date.day! : 1,
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user