From 65b2541063c2f198c13241ee00d059623bd1f4b2 Mon Sep 17 00:00:00 2001 From: Ilya Khait Date: Wed, 28 Feb 2024 21:00:19 +0000 Subject: [PATCH] Implement date ranges & conversion (WiP) --- src/chronology/domain/Date.ts | 128 +---------------- src/chronology/domain/DateBase.ts | 112 ++++++++++----- src/chronology/domain/DateRange.ts | 211 ++++++++++++++++++++++++---- src/chronology/domain/DateString.ts | 127 +++++++++++++++++ 4 files changed, 390 insertions(+), 188 deletions(-) create mode 100644 src/chronology/domain/DateString.ts diff --git a/src/chronology/domain/Date.ts b/src/chronology/domain/Date.ts index cbadae22b..070948031 100644 --- a/src/chronology/domain/Date.ts +++ b/src/chronology/domain/Date.ts @@ -1,132 +1,8 @@ import { MesopotamianDateDto } from 'fragmentarium/domain/FragmentDtos' -import _ from 'lodash' -import { romanize } from 'romans' -import { MesopotamianDateBase } from 'chronology/domain/DateBase' +import { MesopotamianDateString } from 'chronology/domain/DateString' -export class MesopotamianDate extends MesopotamianDateBase { +export class MesopotamianDate extends MesopotamianDateString { static fromJson(dateJson: MesopotamianDateDto): MesopotamianDate { return new MesopotamianDate({ ...dateJson }) } - - toString(): string { - const dayMonthYear = this.dayMonthYearToString().join('.') - const dateTail = `${this.kingEponymOrEraToString()}${this.ur3CalendarToString()}${this.modernDateToString()}` - return [dayMonthYear, dateTail] - .filter((string) => !_.isEmpty(string)) - .join(' ') - } - - private modernDateToString(): string { - const julianDate = this.toModernDate('Julian') - const gregorianDate = this.toModernDate('Gregorian') - return julianDate && - gregorianDate && - gregorianDate.replace('PGC', 'PJC') !== julianDate - ? ` (${[julianDate, gregorianDate].join(' | ')})` - : julianDate - ? ` (${julianDate})` - : '' - } - - private dayMonthYearToString(): string[] { - const fields = ['day', 'month', 'year'] - const emptyParams = fields.map((field) => { - const { isBroken, isUncertain, value } = this[field] - return !isBroken && !isUncertain && _.isEmpty(value) - }) - if (!emptyParams.includes(false)) { - return [] - } - return fields.map((field) => - this.datePartToString(field as 'year' | 'day' | 'month') - ) - } - - private parameterToString( - field: 'year' | 'day' | 'month', - element?: string - ): string { - element = - !_.isEmpty(element) && typeof element == 'string' - ? element - : !_.isEmpty(this[field].value) - ? this[field].value - : '∅' - return this.brokenAndUncertainToString(field, element) - } - - private brokenAndUncertainToString( - field: 'year' | 'day' | 'month', - element: string - ): string { - const { isBroken, isUncertain, value } = this[field] - let brokenIntercalary = '' - if (isBroken && !value) { - element = 'x' - brokenIntercalary = - field === 'month' && this.month.isIntercalary ? '²' : '' - } - return this.getBrokenAndUncertainString({ - element, - brokenIntercalary, - isBroken, - isUncertain, - }) - } - - private getBrokenAndUncertainString({ - element, - brokenIntercalary = '', - isBroken, - isUncertain, - }: { - element: string - brokenIntercalary?: string - isBroken?: boolean - isUncertain?: boolean - }): string { - return `${isBroken ? '[' : ''}${element}${ - isBroken ? ']' + brokenIntercalary : '' - }${isUncertain ? '?' : ''}` - } - - datePartToString(part: 'year' | 'month' | 'day'): string { - if (part === 'month') { - const month = Number(this.month.value) - ? romanize(Number(this.month.value)) - : this.month.value - const intercalary = this.month.isIntercalary ? '²' : '' - return this.parameterToString('month', month + intercalary) - } - return this.parameterToString(part) - } - - private eponymToString(): string { - return `${this.getBrokenAndUncertainString({ - element: this?.eponym?.name ?? '', - ...this?.eponym, - })} (${this?.eponym?.phase} eponym)` - } - - private kingToString(): string { - return this.getBrokenAndUncertainString({ - element: this.king?.name ?? '', - ...this?.king, - }) - } - - kingEponymOrEraToString(): string { - if (this.isSeleucidEra) { - return 'SE' - } else if (this.isAssyrianDate && this.eponym?.name) { - return this.eponymToString() - } else if (this.king?.name) { - return this.kingToString() - } - return '' - } - - ur3CalendarToString(): string { - return this.ur3Calendar ? `, ${this.ur3Calendar} calendar` : '' - } } diff --git a/src/chronology/domain/DateBase.ts b/src/chronology/domain/DateBase.ts index 914608108..3fe9279bd 100644 --- a/src/chronology/domain/DateBase.ts +++ b/src/chronology/domain/DateBase.ts @@ -3,6 +3,7 @@ import { Eponym } from 'chronology/ui/DateEditor/Eponyms' import DateConverter from 'chronology/domain/DateConverter' import data from 'chronology/domain/dateConverterData.json' import _ from 'lodash' +import DateRange from './DateRange' export interface DateField { value: string @@ -32,6 +33,13 @@ interface DateProps { calendar: 'Julian' | 'Gregorian' } +export enum DateType { + seleucidDate = 'seleucidDate', + nabonassarEraDate = 'nabonassarEraDate', + assyrianDate = 'assyrianDate', + kingDate = 'kingDate', +} + export enum Ur3Calendar { ADAB = 'Adab', GIRSU = 'Girsu', @@ -54,6 +62,7 @@ export class MesopotamianDateBase { isSeleucidEra?: boolean isAssyrianDate?: boolean ur3Calendar?: Ur3Calendar + range?: DateRange constructor({ year, @@ -82,10 +91,15 @@ export class MesopotamianDateBase { this.isSeleucidEra = isSeleucidEra this.isAssyrianDate = isAssyrianDate this.ur3Calendar = ur3Calendar + if (this.getEmptyOrBrokenFields().length > 0 && this.getType() !== null) { + this.range = DateRange.getRangeFromPartialDate(this) + console.log('!! Range:', this.range, this.range.toDateString()) + } } - private isSeleucidEraApplicable(year: number): boolean { - return !!this.isSeleucidEra && year > 0 + private isSeleucidEraApplicable(year?: number | string): boolean { + year = typeof year === 'number' ? year : parseInt(year ?? '') + return !!this.isSeleucidEra && !isNaN(year) && year > 0 } private isNabonassarEraApplicable(): boolean { @@ -103,44 +117,48 @@ export class MesopotamianDateBase { return !!this.king?.date } - toModernDate(calendar: 'Julian' | 'Gregorian' = 'Julian'): string { - const dateProps = { - ...this.getDateApproximation(), - calendar, - } - let julianDate = '' - if (this.isSeleucidEraApplicable(dateProps.year)) { - julianDate = this.seleucidToModernDate(dateProps) + getType(): DateType | null { + if (this?.year?.value && this.isSeleucidEraApplicable(this?.year?.value)) { + return DateType.seleucidDate } else if (this.isNabonassarEraApplicable()) { - julianDate = this.getNabonassarEraDate(dateProps) + return DateType.nabonassarEraDate } else if (this.isAssyrianDateApplicable()) { - julianDate = this.getAssyrianDate({ calendar: 'Julian' }) + return DateType.assyrianDate } else if (this.isKingDateApplicable()) { - julianDate = this.kingToModernDate({ ...dateProps, calendar: 'Julian' }) + return DateType.kingDate } - return julianDate + return null } - private getNabonassarEraDate({ - year, - month, - day, - isApproximate, - calendar, - }: DateProps): string { - return this.nabonassarEraToModernDate({ - year: year > 0 ? year : 1, - month, - day, - isApproximate, + toModernDate(calendar: 'Julian' | 'Gregorian' = 'Julian'): string { + const type = this.getType() + if (type === null) { + return '' + } + const dateProps = { + ...this.getDateApproximation(), calendar, - }) + } + const { year } = dateProps + return { + seleucidDate: () => this.seleucidToModernDate(dateProps), + nabonassarEraDate: () => + this.nabonassarEraToModernDate({ + ...dateProps, + year: year > 0 ? year : 1, + }), + assyrianDate: () => this.getAssyrianDate({ calendar: 'Julian' }), + kingDate: () => + this.kingToModernDate({ ...dateProps, calendar: 'Julian' }), + }[type]() } private getAssyrianDate({ calendar = 'Julian', }: Pick): string { return `ca. ${this.eponym?.date} BCE ${calendarToAbbreviation(calendar)}` + // ToDo: Continue here + // Calculate years in range if year is missing } private getDateApproximation(): { @@ -152,19 +170,28 @@ export class MesopotamianDateBase { const year = parseInt(this.year.value) const month = parseInt(this.month.value) const day = parseInt(this.day.value) - // ToDo: Change this to ranges - // Implement `getDateRangeFromPartialDate` - // And use it. - // If possible, try to adjust to exact ranges - // that differ from scenario to scenario + const isApproximate = this.isApproximate() + // ToDo: Change this return { year: isNaN(year) ? -1 : year, month: isNaN(month) ? 1 : month, day: isNaN(day) ? 1 : day, - isApproximate: this.isApproximate(), + isApproximate, } } + getEmptyOrBrokenFields(): Array<'year' | 'month' | 'day'> { + const fields: Array<'year' | 'month' | 'day'> = ['year', 'month', 'day'] + return fields + .map((field) => { + if (isNaN(parseInt(this[field].value)) || this[field].isBroken) { + return field + } + return null + }) + .filter((field) => !!field) as Array<'year' | 'month' | 'day'> + } + private isApproximate(): boolean { return [ _.some( @@ -193,6 +220,8 @@ export class MesopotamianDateBase { isApproximate, calendar, }: DateProps): string { + // ToDo: Continue here + // Use converter to compute earliest and latest dates in range const converter = new DateConverter() converter.setToSeBabylonianDate(year, month, day) return this.insertDateApproximation( @@ -208,12 +237,11 @@ export class MesopotamianDateBase { isApproximate, calendar, }: DateProps): string { - const kingName = Object.keys(data.rulerToBrinkmanKings).find( - (key) => data.rulerToBrinkmanKings[key] === this.king?.orderGlobal - ) - if (kingName) { + if (this.kingName) { + // ToDo: Continue here + // Use converter to compute earliest and latest dates in range const converter = new DateConverter() - converter.setToMesopotamianDate(kingName, year, month, day) + converter.setToMesopotamianDate(this.kingName, year, month, day) return this.insertDateApproximation( converter.toDateString(calendar), isApproximate @@ -222,6 +250,12 @@ export class MesopotamianDateBase { return '' } + get kingName(): string | undefined { + return Object.keys(data.rulerToBrinkmanKings).find( + (key) => data.rulerToBrinkmanKings[key] === this.king?.orderGlobal + ) + } + private kingToModernDate({ year, calendar = 'Julian', @@ -234,6 +268,8 @@ export class MesopotamianDateBase { : this.king?.date && !['', '?'].includes(this.king?.date) ? `ca. ${this.king?.date} BCE ${calendarToAbbreviation(calendar)}` : '' + // ToDo: Continue here + // Calculate years in range if year is missing } private insertDateApproximation( diff --git a/src/chronology/domain/DateRange.ts b/src/chronology/domain/DateRange.ts index 454a6a8f6..bc83175d5 100644 --- a/src/chronology/domain/DateRange.ts +++ b/src/chronology/domain/DateRange.ts @@ -1,28 +1,191 @@ -import { MesopotamianDate } from 'chronology/domain/Date' - -export function getDateRangeFromPartialDate(date: MesopotamianDate): DateRange { - // ToDo: - // Use ranges when calculating approximation. - const startDate = new MesopotamianDate({ ...date }) - const endDate = new MesopotamianDate({ ...date }) - return new DateRange({ start: startDate, end: endDate }) -} +import { DateType, MesopotamianDateBase } from 'chronology/domain/DateBase' +import DateConverter from './DateConverter' +import { CalendarProps } from './DateConverterBase' export default class DateRange { - start: MesopotamianDate - end: MesopotamianDate - constructor({ - start, - end, - }: { - start: MesopotamianDate - end: MesopotamianDate - }) { - this.start = start - this.end = end - } - - toString(): string { - return `${this.start.toString()} - ${this.start.toString()}` + start: CalendarProps + end: CalendarProps + private _converter: DateConverter + + constructor() { + this._converter = new DateConverter() + this.start = this._converter.calendar + this.end = this._converter.calendar + } + + toDateString(calendarType?: 'Julian' | 'Gregorian'): string { + this._converter.setToCjdn(this.start.cjdn) + const startDateString = this._converter.toDateString(calendarType) + this._converter.setToCjdn(this.end.cjdn) + const endDateString = this._converter.toDateString(calendarType) + const endDateArray = endDateString.split(' ').reverse() + const startDateArray: string[] = [] + for (const [index, startElement] of startDateString + .split(' ') + .reverse() + .entries()) { + const endElement = endDateArray[index] + if (startElement !== endElement) { + startDateArray.push(startElement) + } + } + // ToDo: Continue here. Verify that it works as expected + return `${startDateArray.reverse().join(' ')} - ${endDateString}` + } + + private setToPartialDate(date: MesopotamianDateBase): void { + const fieldsToUpdate = date.getEmptyOrBrokenFields() + const defaultValues = this.getDefaultDateValues(date) + + const startDate = this.calculateDateValues( + fieldsToUpdate, + defaultValues, + 'start', + date + ) + const endDate = this.calculateDateValues( + fieldsToUpdate, + defaultValues, + 'end', + date + ) + + this.setDateBasedOnType(date, startDate, 'start') + this.setDateBasedOnType(date, endDate, 'end') + } + + private getDefaultDateValues( + date: MesopotamianDateBase + ): { year: number; month: number; day: number } { + const parseValue = (value: { value: string }) => parseInt(value.value) + return { + year: parseValue(date.year), + month: parseValue(date.month), + day: parseValue(date.day), + } + } + + private calculateDateValues( + fields: Array<'year' | 'month' | 'day'>, + defaultValues: { year: number; month: number; day: number }, + type: 'start' | 'end', + date: MesopotamianDateBase + ): { year: number; month: number; day: number } { + return fields.reduce( + (acc, field) => ({ + ...acc, + [field]: + type === 'start' + ? 1 + : parseInt(this.getEndValueForPartialDate(date, field)), + }), + defaultValues + ) + } + + private setDateBasedOnType( + date: MesopotamianDateBase, + dateValues: { year: number; month: number; day: number }, + type: 'start' | 'end' + ): void { + if (date.getType() === DateType.seleucidDate) { + this._converter.setToSeBabylonianDate( + dateValues.year, + dateValues.month, + dateValues.day + ) + } else if (date.getType() === DateType.nabonassarEraDate) { + this._converter.setToMesopotamianDate( + date.kingName as string, + dateValues.year, + dateValues.month, + dateValues.day + ) + } + + this[type] = { ...this._converter.calendar } + } + + private getEndValueForPartialDate( + date: MesopotamianDateBase, + field: 'year' | 'month' | 'day' + ): string { + const dateType = date.getType() + if (dateType === DateType.seleucidDate) { + return { + year: () => `${this.seleucidRangeEndYear}`, + month: () => `${this.getSeleucidDateEndMonth(date)}`, + day: () => `${this.getSeleucidDateEndDay(date)}`, + }[field]() + } else if (DateType.nabonassarEraDate) { + return { + year: () => `${this.getNabonassarRangeEndYear}`, + month: () => `${this.getNabonassarDateEndMonth(date)}`, + day: () => `${this.getNabonassarDateEndDay(date)}`, + }[field]() + } + return '' + } + + private get seleucidRangeEndYear(): number { + return this._converter.latestDate.seBabylonianYear + } + + private getSeleucidRangeEndYear(date: MesopotamianDateBase): number { + return date.getEmptyOrBrokenFields().includes('year') + ? this.seleucidRangeEndYear + : parseInt(date.year.value) + } + + private getSeleucidDateEndMonth(date: MesopotamianDateBase): number { + const year = this.getSeleucidRangeEndYear(date) + return date.getEmptyOrBrokenFields().includes('month') + ? this._converter.getMesopotamianMonthsOfSeYear(year).length + : parseInt(date.month.value) + } + + private getSeleucidDateEndDay(date: MesopotamianDateBase): number { + const year = this.getSeleucidRangeEndYear(date) + const month = this.getSeleucidDateEndMonth(date) + this._converter.setToSeBabylonianDate(year, month, 1) + return this._converter.calendar.mesopotamianMonthLength ?? 29 + } + + private get nabonassarRangeEndYear(): number { + return this._converter.latestDate.regnalYears ?? 1 + } + + private getNabonassarRangeEndYear(date: MesopotamianDateBase): number { + return date.getEmptyOrBrokenFields().includes('year') + ? this.nabonassarRangeEndYear + : parseInt(date.year.value) + } + + private getNabonassarDateEndMonth(date: MesopotamianDateBase): number { + const year = this.getNabonassarRangeEndYear(date) + this._converter.setToMesopotamianDate(date.kingName as string, year, 1, 1) + return date.getEmptyOrBrokenFields().includes('month') + ? this._converter.getMesopotamianMonthsOfSeYear( + this._converter.calendar.seBabylonianYear + ).length + : parseInt(date.month.value) + } + + private getNabonassarDateEndDay(date: MesopotamianDateBase): number { + const year = this.getNabonassarRangeEndYear(date) + const month = this.getNabonassarDateEndMonth(date) + this._converter.setToMesopotamianDate( + date.kingName as string, + year, + month, + 1 + ) + return this._converter.calendar.mesopotamianMonthLength ?? 29 + } + + static getRangeFromPartialDate(date: MesopotamianDateBase): DateRange { + const range = new DateRange() + range.setToPartialDate(date) + return range } } diff --git a/src/chronology/domain/DateString.ts b/src/chronology/domain/DateString.ts new file mode 100644 index 000000000..6c338c354 --- /dev/null +++ b/src/chronology/domain/DateString.ts @@ -0,0 +1,127 @@ +import _ from 'lodash' +import { romanize } from 'romans' +import { MesopotamianDateBase } from 'chronology/domain/DateBase' + +export class MesopotamianDateString extends MesopotamianDateBase { + toString(): string { + const dayMonthYear = this.dayMonthYearToString().join('.') + const dateTail = `${this.kingEponymOrEraToString()}${this.ur3CalendarToString()}${this.modernDateToString()}` + return [dayMonthYear, dateTail] + .filter((string) => !_.isEmpty(string)) + .join(' ') + } + + private modernDateToString(): string { + const julianDate = this.toModernDate('Julian') + const gregorianDate = this.toModernDate('Gregorian') + return julianDate && + gregorianDate && + gregorianDate.replace('PGC', 'PJC') !== julianDate + ? ` (${[julianDate, gregorianDate].join(' | ')})` + : julianDate + ? ` (${julianDate})` + : '' + } + + private dayMonthYearToString(): string[] { + const fields = ['day', 'month', 'year'] + const emptyParams = fields.map((field) => { + const { isBroken, isUncertain, value } = this[field] + return !isBroken && !isUncertain && _.isEmpty(value) + }) + if (!emptyParams.includes(false)) { + return [] + } + return fields.map((field) => + this.datePartToString(field as 'year' | 'day' | 'month') + ) + } + + private parameterToString( + field: 'year' | 'day' | 'month', + element?: string + ): string { + element = + !_.isEmpty(element) && typeof element == 'string' + ? element + : !_.isEmpty(this[field].value) + ? this[field].value + : '∅' + return this.brokenAndUncertainToString(field, element) + } + + private brokenAndUncertainToString( + field: 'year' | 'day' | 'month', + element: string + ): string { + const { isBroken, isUncertain, value } = this[field] + let brokenIntercalary = '' + if (isBroken && !value) { + element = 'x' + brokenIntercalary = + field === 'month' && this.month.isIntercalary ? '²' : '' + } + return this.getBrokenAndUncertainString({ + element, + brokenIntercalary, + isBroken, + isUncertain, + }) + } + + private getBrokenAndUncertainString({ + element, + brokenIntercalary = '', + isBroken, + isUncertain, + }: { + element: string + brokenIntercalary?: string + isBroken?: boolean + isUncertain?: boolean + }): string { + return `${isBroken ? '[' : ''}${element}${ + isBroken ? ']' + brokenIntercalary : '' + }${isUncertain ? '?' : ''}` + } + + datePartToString(part: 'year' | 'month' | 'day'): string { + if (part === 'month') { + const month = Number(this.month.value) + ? romanize(Number(this.month.value)) + : this.month.value + const intercalary = this.month.isIntercalary ? '²' : '' + return this.parameterToString('month', month + intercalary) + } + return this.parameterToString(part) + } + + private eponymToString(): string { + return `${this.getBrokenAndUncertainString({ + element: this?.eponym?.name ?? '', + ...this?.eponym, + })} (${this?.eponym?.phase} eponym)` + } + + private kingToString(): string { + return this.getBrokenAndUncertainString({ + element: this.king?.name ?? '', + ...this?.king, + }) + } + + kingEponymOrEraToString(): string { + if (this.isSeleucidEra) { + return 'SE' + } else if (this.isAssyrianDate && this.eponym?.name) { + return this.eponymToString() + } else if (this.king?.name) { + return this.kingToString() + } + return '' + } + + ur3CalendarToString(): string { + return this.ur3Calendar ? `, ${this.ur3Calendar} calendar` : '' + } +}