Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/expressions/dataTypes/valueTypes/AbstractDuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ abstract class AbstractDuration {
public isPositive() {
return true;
}
public negate() {
return this;
}
}

export default AbstractDuration;
107 changes: 101 additions & 6 deletions src/expressions/dataTypes/valueTypes/DateTime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,14 +466,109 @@ export function subtract(
return new DayTimeDuration(secondsOfDuration);
}

export function addDuration(dateTime: DateTime, _duration: AbstractDuration): DateTime {
throw new Error(`Not implemented: adding durations to ${valueTypeToString(dateTime.type)}`);
export function evalDuration(dateTime: DateTime, duration: AbstractDuration): DateTime {
const tz = dateTime.getTimezone();

let years = dateTime.getYear();
let months = dateTime.getMonth();
let days = dateTime.getDay();
let hours = dateTime.getHours();
let minutes = dateTime.getMinutes();
let seconds = dateTime.getSeconds();
const fraction = dateTime.getSecondFraction();

// Add years and months
years += duration.getYears();
months += duration.getMonths();

// Normalize months
while (months > 12) {
months -= 12;
years += 1;
}
while (months < 1) {
months += 12;
years -= 1;
}

function isLeapYear(year: number): boolean {
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
}
function getLastDayOfMonth(year: number, month: number): number {
if (month === 2) {
return isLeapYear(year) ? 29 : 28;
}
return [4, 6, 9, 11].includes(month) ? 30 : 31;
}

const originalLastDay = getLastDayOfMonth(dateTime.getYear(), dateTime.getMonth());
const originalWasLastDay = dateTime.getDay() === originalLastDay;

// Clamp day to last valid day of new month/year ONLY if original date was last day of its month
const newLastDay = getLastDayOfMonth(years, months);
if (originalWasLastDay) {
days = newLastDay;
}

// Add days, hours, minutes, seconds, fraction
days += duration.getDays();
hours += duration.getHours();
minutes += duration.getMinutes();
seconds += duration.getSeconds();

// Normalize seconds
if (seconds >= 60) {
minutes += Math.floor(seconds / 60);
seconds = seconds % 60;
} else if (seconds < 0) {
minutes -= Math.ceil(Math.abs(seconds) / 60);
seconds = ((seconds % 60) + 60) % 60;
}

// Normalize minutes
if (minutes >= 60) {
hours += Math.floor(minutes / 60);
minutes = minutes % 60;
} else if (minutes < 0) {
hours -= Math.ceil(Math.abs(minutes) / 60);
minutes = ((minutes % 60) + 60) % 60;
}

// Normalize hours
if (hours >= 24) {
days += Math.floor(hours / 24);
hours = hours % 24;
} else if (hours < 0) {
days -= Math.ceil(Math.abs(hours) / 24);
hours = ((hours % 24) + 24) % 24;
}

while (days > getLastDayOfMonth(years, months)) {
days -= getLastDayOfMonth(years, months);
months += 1;
}
while (days < 1) {
months -= 1;
days += getLastDayOfMonth(years, months);
}

while (months > 12) {
months -= 12;
years += 1;
}
while (months < 1) {
months += 12;
years -= 1;
}

return new DateTime(years, months, days, hours, minutes, seconds, fraction, tz, dateTime.type);
}
export function addDuration(dateTime: DateTime, duration: AbstractDuration): DateTime {
return evalDuration(dateTime, duration);
}

export function subtractDuration(dateTime: DateTime, _duration: AbstractDuration): DateTime {
throw new Error(
`Not implemented: subtracting durations from ${valueTypeToString(dateTime.type)}`,
);
export function subtractDuration(dateTime: DateTime, duration: AbstractDuration): DateTime {
return evalDuration(dateTime, duration.negate());
}

export default DateTime;
4 changes: 4 additions & 0 deletions src/expressions/dataTypes/valueTypes/DayTimeDuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ class DayTimeDuration extends AbstractDuration {
return Object.is(-0, this.seconds) ? false : this.seconds >= 0;
}

public negate(): this {
return new (this.constructor as any)(-this.seconds);
Copy link
Member

@bwrrp bwrrp Sep 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes any subclass uses the same argument for its constructor - why not use the class itself directly?

}

public toString() {
return (this.isPositive() ? 'P' : '-P') + this.toStringWithoutP();
}
Expand Down
7 changes: 7 additions & 0 deletions src/expressions/dataTypes/valueTypes/Duration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@ class Duration extends AbstractDuration {
return this._yearMonthDuration.isPositive() && this._dayTimeDuration.isPositive();
}

public negate(): this {
return new (this.constructor as any)(
this._yearMonthDuration.negate(),
this._dayTimeDuration.negate(),
);
}

public toString() {
const durationString = this.isPositive() ? 'P' : '-P';
const TYM = this._yearMonthDuration.toStringWithoutP();
Expand Down
4 changes: 4 additions & 0 deletions src/expressions/dataTypes/valueTypes/YearMonthDuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ class YearMonthDuration extends AbstractDuration {
return Object.is(-0, this.months) ? false : this.months >= 0;
}

public negate(): this {
return new (this.constructor as any)(-this.months);
}

public toString() {
return (this.isPositive() ? 'P' : '-P') + this.toStringWithoutP();
}
Expand Down
45 changes: 0 additions & 45 deletions test/assets/unrunnableTestCases.csv

Large diffs are not rendered by default.

200 changes: 199 additions & 1 deletion test/specs/expressions/dataTypes/valueTypes/DateTime.tests.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import * as chai from 'chai';
import DateTime from 'fontoxpath/expressions/dataTypes/valueTypes/DateTime';
import DateTime, {
addDuration,
subtractDuration,
} from 'fontoxpath/expressions/dataTypes/valueTypes/DateTime';
import DayTimeDuration from 'fontoxpath/expressions/dataTypes/valueTypes/DayTimeDuration';
import Duration from 'fontoxpath/expressions/dataTypes/valueTypes/Duration';

describe('Data type: dateTime', () => {
describe('DateTime.fromString()', () => {
Expand Down Expand Up @@ -52,5 +56,199 @@ describe('Data type: dateTime', () => {
),
);
});

it('addDuration "P1Y2M" to "1999-12-31T23:00:00+10:00"', () => {
const dateTime = DateTime.fromString('1999-12-31T23:00:00+10:00');
const duration = Duration.fromString('P1Y2M');
const newDateTime = addDuration(dateTime, duration);
chai.assert.deepEqual(
newDateTime,
new DateTime(
2001,
2,
28,
23,
0,
0,
0,
DayTimeDuration.fromTimezoneString('+10:00'),
),
);
});

it('addDuration "P3DT1H15M" to "1999-12-31T23:00:00+10:00"', () => {
const dateTime = DateTime.fromString('1999-12-31T23:00:00+10:00');
const duration = Duration.fromString('P3DT1H15M');
const newDateTime = addDuration(dateTime, duration);
chai.assert.deepEqual(
newDateTime,
// eslint-disable-next-line prettier/prettier
new DateTime(
2000,
1,
4,
0,
15,
0,
0,
DayTimeDuration.fromTimezoneString('+10:00'),
),
);
});

it('addDuration "PT505H" to "1999-12-31T23:00:00+10:00"', () => {
const dateTime = DateTime.fromString('1999-12-31T23:00:00+10:00');
const duration = Duration.fromString('PT505H');
const newDateTime = addDuration(dateTime, duration);
chai.assert.deepEqual(
newDateTime,
// eslint-disable-next-line prettier/prettier
new DateTime(
2000,
1,
22,
0,
0,
0,
0,
DayTimeDuration.fromTimezoneString('+10:00'),
),
);
});

it('addDuration "P60D" to "1999-12-31T23:00:00+10:00"', () => {
const dateTime = DateTime.fromString('1999-12-31T23:00:00+10:00');
const duration = Duration.fromString('P60D');
const newDateTime = addDuration(dateTime, duration);
chai.assert.deepEqual(
newDateTime,
// eslint-disable-next-line prettier/prettier
new DateTime(
2000,
2,
29,
23,
0,
0,
0,
DayTimeDuration.fromTimezoneString('+10:00'),
),
);
});

it('addDuration with negative "-P3DT1H15M" to "2000-01-04T00:15:00+10:00"', () => {
const dateTime = DateTime.fromString('2000-01-04T00:15:00+10:00');
const duration = Duration.fromString('-P3DT1H15M');
const newDateTime = addDuration(dateTime, duration);
chai.assert.deepEqual(
newDateTime,
new DateTime(
1999,
12,
31,
23,
0,
0,
0,
DayTimeDuration.fromTimezoneString('+10:00'),
),
);
});

it('subDuration "P1Y2M" from "2001-02-28T23:00:00+10:00"', () => {
const dateTime = DateTime.fromString('2001-02-28T23:00:00+10:00');
const duration = Duration.fromString('P1Y2M');
const newDateTime = subtractDuration(dateTime, duration);
chai.assert.deepEqual(
newDateTime,
new DateTime(
1999,
12,
31,
23,
0,
0,
0,
DayTimeDuration.fromTimezoneString('+10:00'),
),
);
});
it('subDuration "P3DT1H15M" from "2000-01-04T00:15:00+10:00"', () => {
const dateTime = DateTime.fromString('2000-01-04T00:15:00+10:00');
const duration = Duration.fromString('P3DT1H15M');
const newDateTime = subtractDuration(dateTime, duration);
chai.assert.deepEqual(
newDateTime,
new DateTime(
1999,
12,
31,
23,
0,
0,
0,
DayTimeDuration.fromTimezoneString('+10:00'),
),
);
});

it('subDuration negativ "-P3DT1H15M" to "1999-12-31T23:00:00+10:00"', () => {
const dateTime = DateTime.fromString('1999-12-31T23:00:00+10:00');
const duration = Duration.fromString('-P3DT1H15M');
const newDateTime = subtractDuration(dateTime, duration);
chai.assert.deepEqual(
newDateTime,
// eslint-disable-next-line prettier/prettier
new DateTime(
2000,
1,
4,
0,
15,
0,
0,
DayTimeDuration.fromTimezoneString('+10:00'),
),
);
});
});
it('subDuration "PT505H" to "2000-01-22T00:00:00+10:00"', () => {
const dateTime = DateTime.fromString('2000-01-22T00:00:00+10:00');
const duration = Duration.fromString('PT505H');
const newDateTime = subtractDuration(dateTime, duration);
chai.assert.deepEqual(
newDateTime,
// eslint-disable-next-line prettier/prettier
new DateTime(
1999,
12,
31,
23,
0,
0,
0,
DayTimeDuration.fromTimezoneString('+10:00'),
),
);
});

it('subDuration "P60D" to "2000-02-29T23:00:00+10:00"', () => {
const dateTime = DateTime.fromString('2000-02-29T23:00:00+10:00');
const duration = Duration.fromString('P60D');
const newDateTime = subtractDuration(dateTime, duration);
chai.assert.deepEqual(
newDateTime,
// eslint-disable-next-line prettier/prettier
new DateTime(
1999,
12,
31,
23,
0,
0,
0,
DayTimeDuration.fromTimezoneString('+10:00'),
),
);
});
});
Loading
Loading