diff --git a/src/expressions/dataTypes/valueTypes/AbstractDuration.ts b/src/expressions/dataTypes/valueTypes/AbstractDuration.ts
index d6818fa6a..8104cb197 100644
--- a/src/expressions/dataTypes/valueTypes/AbstractDuration.ts
+++ b/src/expressions/dataTypes/valueTypes/AbstractDuration.ts
@@ -40,6 +40,9 @@ abstract class AbstractDuration {
public isPositive() {
return true;
}
+ public negate() {
+ return this;
+ }
}
export default AbstractDuration;
diff --git a/src/expressions/dataTypes/valueTypes/DateTime.ts b/src/expressions/dataTypes/valueTypes/DateTime.ts
index fb7a91b0a..148a6530e 100644
--- a/src/expressions/dataTypes/valueTypes/DateTime.ts
+++ b/src/expressions/dataTypes/valueTypes/DateTime.ts
@@ -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;
diff --git a/src/expressions/dataTypes/valueTypes/DayTimeDuration.ts b/src/expressions/dataTypes/valueTypes/DayTimeDuration.ts
index 6f6c610f8..97f534e4d 100644
--- a/src/expressions/dataTypes/valueTypes/DayTimeDuration.ts
+++ b/src/expressions/dataTypes/valueTypes/DayTimeDuration.ts
@@ -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);
+ }
+
public toString() {
return (this.isPositive() ? 'P' : '-P') + this.toStringWithoutP();
}
diff --git a/src/expressions/dataTypes/valueTypes/Duration.ts b/src/expressions/dataTypes/valueTypes/Duration.ts
index 1439798fd..bc55238c5 100644
--- a/src/expressions/dataTypes/valueTypes/Duration.ts
+++ b/src/expressions/dataTypes/valueTypes/Duration.ts
@@ -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();
diff --git a/src/expressions/dataTypes/valueTypes/YearMonthDuration.ts b/src/expressions/dataTypes/valueTypes/YearMonthDuration.ts
index f46a24dea..f819d6281 100644
--- a/src/expressions/dataTypes/valueTypes/YearMonthDuration.ts
+++ b/src/expressions/dataTypes/valueTypes/YearMonthDuration.ts
@@ -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();
}
diff --git a/test/assets/unrunnableTestCases.csv b/test/assets/unrunnableTestCases.csv
index 80dc07251..886601f44 100644
--- a/test/assets/unrunnableTestCases.csv
+++ b/test/assets/unrunnableTestCases.csv
@@ -252,14 +252,6 @@ fn-countnpi1args-1,Error: FOCA0003: can not cast -999999999999999999 to xs:integ
fn-countnpi1args-2,Error: FOCA0003: can not cast -475688437271870490 to xs:integer, it is out of bounds for JavaScript numbers.
fn-countnni1args-2,Error: FOCA0003: can not cast 303884545991464527 to xs:integer, it is out of bounds for JavaScript numbers.
fn-countnni1args-3,Error: FOCA0003: can not cast 999999999999999999 to xs:integer, it is out of bounds for JavaScript numbers.
-fn-current-date-6,Error: Not implemented: adding durations to xs:date
-fn-current-date-7,Error: Not implemented: subtracting durations from xs:date
-fn-current-date-21,Error: Not implemented: subtracting durations from xs:date
-fn-current-dateTime-6,Error: Not implemented: adding durations to xs:dateTime
-fn-current-datetime-7,Error: Not implemented: subtracting durations from xs:dateTime
-fn-current-dateTime-21,Error: Not implemented: subtracting durations from xs:dateTime
-fn-current-time-6,Error: Not implemented: adding durations to xs:time
-fn-current-time-7,Error: Not implemented: subtracting durations from xs:time
fn-dataintg1args-1,Error: FOCA0003: can not cast -999999999999999999 to xs:integer, it is out of bounds for JavaScript numbers.
fn-dataintg1args-2,Error: FOCA0003: can not cast 830993497117024304 to xs:integer, it is out of bounds for JavaScript numbers.
fn-dataintg1args-3,Error: FOCA0003: can not cast 999999999999999999 to xs:integer, it is out of bounds for JavaScript numbers.
@@ -278,10 +270,6 @@ fn-datanni1args-2,Error: FOCA0003: can not cast 303884545991464527 to xs:integer
fn-datanni1args-3,Error: FOCA0003: can not cast 999999999999999999 to xs:integer, it is out of bounds for JavaScript numbers.
K2-DataFunc-6,AssertionError: expected [Function] to throw an error
fn-dateTime-22,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}adjust-dateTime-to-timezone with arity of 2 not registered. Did you mean "Q{test}custom-dateTime-function ()"?
-fn-dateTime-24,Error: Not implemented: adding durations to xs:dateTime
-fn-dateTime-25,Error: Not implemented: adding durations to xs:dateTime
-fn-dateTime-26,Error: Not implemented: subtracting durations from xs:dateTime
-fn-dateTime-27,Error: Not implemented: subtracting durations from xs:dateTime
cbcl-dateTime-001,Error: XPTY0004: eqOp not available for xs:string and xs:dateTime
cbcl-dateTime-002,AssertionError: expected [Function] to throw error matching /FORG0008/ but got 'XPST0017: Function Q{http://www.w3.or…'
fn-day-from-dateTime-3,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}adjust-dateTime-to-timezone with arity of 2 not registered. Did you mean "Q{test}custom-dateTime-function ()"?
@@ -343,7 +331,6 @@ K2-SeqDeepEqualFunc-14,Error: No selector counterpart for: computedDocumentConst
K2-SeqDeepEqualFunc-15,Error: No selector counterpart for: computedDocumentConstructor.
K2-SeqDeepEqualFunc-16,Error: No selector counterpart for: computedDocumentConstructor.
K2-SeqDeepEqualFunc-17,Error: No selector counterpart for: computedDocumentConstructor.
-K2-SeqDeepEqualFunc-40,Error: Not implemented: subtracting durations from xs:dateTime
cbcl-deep-equal-005,Error: 1: declare function local:f($x as xs:integer)as xs:integer* { 1 to $x }; deep-equal((local:f(3), 2, local:f(1)), (local:f(3), 2)) ^ Error: XPST0003: Failed to parse script. Expected {,external at <>:1:43 - 1:44
fn-distinct-valuesintg1args-1,Error: FOCA0003: can not cast -999999999999999999 to xs:integer, it is out of bounds for JavaScript numbers.
fn-distinct-valuesintg1args-2,Error: FOCA0003: can not cast 830993497117024304 to xs:integer, it is out of bounds for JavaScript numbers.
@@ -751,12 +738,6 @@ fn-idref-4,Error: No selector counterpart for: computedDocumentConstructor.
cbcl-idref-001,Error: No selector counterpart for: computedDocumentConstructor.
cbcl-idref-002,AssertionError: Expected executing the XPath " let $doc := document { } return fn:empty( fn:idref( (), $doc) ) " to resolve to one of the expected results, but got Error: No selector counterpart for: computedDocumentConstructor., AssertionError: expected [Function] to throw error matching /XPST0005/ but got 'No selector counterpart for: computed…'.
cbcl-idref-003,Error: No selector counterpart for: computedDocumentConstructor.
-fn-implicit-timezone-15,Error: Not implemented: adding durations to xs:time
-fn-implicit-timezone-16,Error: Not implemented: subtracting durations from xs:time
-fn-implicit-timezone-17,Error: Not implemented: subtracting durations from xs:date
-fn-implicit-timezone-18,Error: Not implemented: adding durations to xs:date
-fn-implicit-timezone-19,Error: Not implemented: subtracting durations from xs:dateTime
-fn-implicit-timezone-20,Error: Not implemented: adding durations to xs:dateTime
fn-implicit-timezone-21,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}adjust-date-to-timezone with arity of 2 not registered. No similar functions found.
fn-implicit-timezone-22,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}adjust-time-to-timezone with arity of 2 not registered. No similar functions found.
fn-implicit-timezone-23,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}adjust-dateTime-to-timezone with arity of 2 not registered. Did you mean "Q{test}custom-dateTime-function ()"?
@@ -1739,11 +1720,6 @@ xs-error-048,AssertionError: expected [Function] to throw error matching /XPDY00
xs-error-049,Error: No selector counterpart for: treatExpr.
xs-float-004,AssertionError: expected [Function] to throw an error
K-DayTimeDurationAdd-3,AssertionError: Expected XPath xs:dayTimeDuration("P3DT4H3M3.100S") + xs:dayTimeDuration("P3DT12H31M56.303S") eq xs:dayTimeDuration("P6DT16H34M59.403S") to resolve to true: expected false to be true
-cbcl-plus-001,Error: Not implemented: adding durations to xs:date
-cbcl-plus-003,Error: Not implemented: adding durations to xs:date
-cbcl-plus-005,Error: Not implemented: adding durations to xs:dateTime
-cbcl-plus-007,Error: Not implemented: adding durations to xs:dateTime
-cbcl-plus-009,Error: Not implemented: adding durations to xs:time
cbcl-plus-015,Error: XPTY0004: addOp not available for types xs:dayTimeDuration and xs:date
cbcl-plus-017,Error: XPTY0004: addOp not available for types xs:yearMonthDuration and xs:date
cbcl-plus-019,Error: XPTY0004: addOp not available for types xs:dayTimeDuration and xs:dateTime
@@ -2198,13 +2174,6 @@ K2-StringEqual-6,Error: No selector counterpart for: treatExpr.
K2-StringLT-1,AssertionError: Expected XPath "" lt "𑅰" to resolve to true: expected false to be true
op-subtract-dates-yielding-DTD-8,AssertionError: xs:date("0001-01-01Z") - xs:date("2005-07-06Z"): expected '-P38172D' to equal '-P732132D'
op-subtract-dateTimes-yielding-DTD-8,AssertionError: xs:dateTime("0001-01-01T01:01:01Z") - xs:dateTime("2005-07-06T12:12:12Z"): expected '-P38172DT11H11M11S' to equal '-P732132DT11H11M11S'
-K2-DayTimeDurationSubtract-1,Error: Not implemented: subtracting durations from xs:time
-K2-DayTimeDurationSubtract-2,Error: Not implemented: subtracting durations from xs:dateTime
-cbcl-minus-001,Error: Not implemented: subtracting durations from xs:date
-cbcl-minus-003,Error: Not implemented: subtracting durations from xs:date
-cbcl-minus-005,Error: Not implemented: subtracting durations from xs:dateTime
-cbcl-minus-007,Error: Not implemented: subtracting durations from xs:dateTime
-cbcl-minus-009,Error: Not implemented: subtracting durations from xs:time
cbcl-subtract-times-003,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}adjust-time-to-timezone with arity of 1 not registered. No similar functions found.
cbcl-subtract-times-004,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}adjust-time-to-timezone with arity of 2 not registered. No similar functions found.
rangeExpr-28,AssertionError: Expected executing the XPath "18446744073709551616 to 18446744073709551620" to resolve to one of the expected results, but got AssertionError: 18446744073709551616 to 18446744073709551620: expected '18446744073709552000' to equal '18446744073709551616 1844674407370955…', AssertionError: expected [Function] to throw an error.
@@ -2342,7 +2311,6 @@ CastAs175,AssertionError: Expected XPath xs:float("5.4321E-100") cast as xs:deci
CastAs201,AssertionError: xs:double("1e8") cast as xs:string: expected '100000000' to equal '1.0E8'
CastAs600,AssertionError: xs:base64Binary("aA+zZ/09") cast as xs:hexBinary: expected '68FB367FD3D' to equal '680FB367FD3D'
CastAs647,AssertionError: Expected executing the XPath "xs:string(2.123456789123456789) cast as xs:decimal" to resolve to one of the expected results, but got AssertionError: xs:string(2.123456789123456789) cast as xs:decimal: expected '2.123456789123457' to equal '2.123456789123456789', AssertionError: expected [Function] to throw an error.
-CastAs670,AssertionError: Expected executing the XPath "let $d1 := '2006-07-12' cast as xs:date let $oneky := xs:yearMonthDuration('P1000Y') let $d2 := $d1 + $oneky let $d3 := $d2 + $oneky let $d4 := $d3 + $oneky let $d5 := $d4 + $oneky let $d6 := $d5 + $oneky let $d7 := $d6 + $oneky let $d8 := $d7 + $oneky let $d9 := $d8 + $oneky let $d10 := $d9 + $oneky return $d10" to resolve to one of the expected results, but got Error: Not implemented: adding durations to xs:date, AssertionError: expected [Function] to throw error matching /FODT0001/ but got 'Not implemented: adding durations to …'.
CastAs673b,Error: Casting to xs:QName is not implemented.
CastAs674a,Error: Casting to xs:QName is not implemented.
CastAs675a,AssertionError: expected [Function] to throw error matching /XPTY0117/ but got 'Casting to xs:QName is not implemente…'
@@ -3463,8 +3431,6 @@ K-ValCompTypeChecking-27,AssertionError: expected [Function] to throw an error
K-ValCompTypeChecking-28,AssertionError: expected [Function] to throw an error
K-ValCompTypeChecking-29,AssertionError: expected [Function] to throw an error
VarDecl020,AssertionError: declare variable $x := 9.999999999999999; $x: expected '9.999999999999998' to equal '9.999999999999999'
-VarDecl046,Error: Not implemented: adding durations to xs:date
-VarDecl048,Error: Not implemented: adding durations to xs:time
VarDecl059,Error: No selector counterpart for: computedDocumentConstructor.
vardeclerr-1,AssertionError: expected [Function] to throw error matching /XQDY0054/ but got 'Maximum call stack size exceeded'
K2-InternalVariablesWithout-1a,AssertionError: expected [Function] to throw error matching /XQDY0054/ but got 'Maximum call stack size exceeded'
@@ -3530,7 +3496,6 @@ K2-ExternalVariablesWith-22a,Error: 1: declare variable $v as element(*, xs:unty
K2-ExternalVariablesWith-23,Error: 1: declare variable $v as element(elementName, xs:anyType?)+ := ; 1 = 1 ^ Error: XPST0003: Failed to parse script. Expected :=, , ,
, ,(: at <>:1:31 - 1:32
extvardef-002b,AssertionError: expected [Function] to throw error matching /XPTY0004/ but got 'FOTY0013: Atomization is not supporte…'
extvardef-003a,Error: FORG0006: items passed to fn:sum are not all numeric.
-extvardef-007,Error: Not implemented: adding durations to xs:date
extvardef-008,AssertionError: expected [Function] to throw error matching /XPDY0002/ but got 'XQDY0054: The variable x is declared …'
extvardef-011,AssertionError: expected [Function] to throw error matching /XQDY0054/ but got 'Maximum call stack size exceeded'
extvardef-011a,AssertionError: expected [Function] to throw error matching /XQDY0054/ but got 'Maximum call stack size exceeded'
@@ -3777,10 +3742,6 @@ functx-fn-root-1,AssertionError: Expected XPath let $in-xml := 123
functx-fn-root-all,Error: No selector counterpart for: computedDocumentConstructor.
functx-fn-sum-3,Error: FORG0006: items passed to fn:sum are not all numeric.
functx-fn-sum-all,Error: FORG0006: items passed to fn:sum are not all numeric.
-functx-functx-add-months-1,Error: Not implemented: adding durations to xs:date
-functx-functx-add-months-2,Error: Not implemented: adding durations to xs:date
-functx-functx-add-months-3,Error: Not implemented: adding durations to xs:date
-functx-functx-add-months-all,Error: Not implemented: adding durations to xs:date
functx-functx-camel-case-to-words-1,AssertionError: declare namespace functx = "http://www.example.com/"; (:~ : Turns a camelCase string into space-separated words : : @author Priscilla Walmsley, Datypic : @version 1.0 : @see http://www.xqueryfunctions.com/xq/functx_camel-case-to-words.html : @param $arg the string to modify : @param $delim the delimiter for the words (e.g. a space) :) declare function functx:camel-case-to-words ( $arg as xs:string? , $delim as xs:string ) as xs:string { concat(substring($arg,1,1), replace(substring($arg,2),'(\p{Lu})', concat($delim, '$1'))) } ; (functx:camel-case-to-words( 'thisIsACamelCaseTerm',' ')): expected 'thisIsACamelCaseTerm' to equal 'this Is A Camel Case Term'
functx-functx-camel-case-to-words-2,AssertionError: declare namespace functx = "http://www.example.com/"; (:~ : Turns a camelCase string into space-separated words : : @author Priscilla Walmsley, Datypic : @version 1.0 : @see http://www.xqueryfunctions.com/xq/functx_camel-case-to-words.html : @param $arg the string to modify : @param $delim the delimiter for the words (e.g. a space) :) declare function functx:camel-case-to-words ( $arg as xs:string? , $delim as xs:string ) as xs:string { concat(substring($arg,1,1), replace(substring($arg,2),'(\p{Lu})', concat($delim, '$1'))) } ; (functx:camel-case-to-words( 'thisIsACamelCaseTerm',',')): expected 'thisIsACamelCaseTerm' to equal 'this,Is,A,Camel,Case,Term'
functx-functx-camel-case-to-words-all,AssertionError: declare namespace functx = "http://www.example.com/"; (:~ : Turns a camelCase string into space-separated words : : @author Priscilla Walmsley, Datypic : @version 1.0 : @see http://www.xqueryfunctions.com/xq/functx_camel-case-to-words.html : @param $arg the string to modify : @param $delim the delimiter for the words (e.g. a space) :) declare function functx:camel-case-to-words ( $arg as xs:string? , $delim as xs:string ) as xs:string { concat(substring($arg,1,1), replace(substring($arg,2),'(\p{Lu})', concat($delim, '$1'))) } ; (functx:camel-case-to-words( 'thisIsACamelCaseTerm',' '), functx:camel-case-to-words( 'thisIsACamelCaseTerm',',')): expected 'thisIsACamelCaseTerm thisIsACamelCase…' to equal 'this Is A Camel Case Term this,Is,A,C…'
@@ -3790,12 +3751,6 @@ functx-functx-dynamic-path-all,Error: No selector counterpart for: computedDocum
functx-functx-get-matches-1,AssertionError: declare namespace functx = "http://www.example.com/"; (:~ : Splits a string into matching and non-matching regions : : @author Priscilla Walmsley, Datypic : @version 1.0 : @see http://www.xqueryfunctions.com/xq/functx_get-matches-and-non-matches.html : @param $string the string to split : @param $regex the pattern :) declare function functx:get-matches-and-non-matches ( $string as xs:string? , $regex as xs:string ) as element()* { let $iomf := functx:index-of-match-first($string, $regex) return if (empty($iomf)) then {$string} else if ($iomf > 1) then ({substring($string,1,$iomf - 1)}, functx:get-matches-and-non-matches( substring($string,$iomf),$regex)) else let $length := string-length($string) - string-length(functx:replace-first($string, $regex,'')) return ({substring($string,1,$length)}, if (string-length($string) > $length) then functx:get-matches-and-non-matches( substring($string,$length + 1),$regex) else ()) } ; (:~ : Return the matching regions of a string : : @author Priscilla Walmsley, Datypic : @version 1.0 : @see http://www.xqueryfunctions.com/xq/functx_get-matches.html : @param $string the string to split : @param $regex the pattern :) declare function functx:get-matches ( $string as xs:string? , $regex as xs:string ) as xs:string* { functx:get-matches-and-non-matches($string,$regex)/ string(self::match) } ; (:~ : The first position of a matching substring : : @author Priscilla Walmsley, Datypic : @version 1.0 : @see http://www.xqueryfunctions.com/xq/functx_index-of-match-first.html : @param $arg the string : @param $pattern the pattern to match :) declare function functx:index-of-match-first ( $arg as xs:string? , $pattern as xs:string ) as xs:integer? { if (matches($arg,$pattern)) then string-length(tokenize($arg, $pattern)[1]) + 1 else () } ; (:~ : Replaces the first match of a pattern : : @author Priscilla Walmsley, Datypic : @version 1.0 : @see http://www.xqueryfunctions.com/xq/functx_replace-first.html : @param $arg the entire string to change : @param $pattern the pattern of characters to replace : @param $replacement the replacement string :) declare function functx:replace-first ( $arg as xs:string? , $pattern as xs:string , $replacement as xs:string ) as xs:string { replace($arg, concat('(^.*?)', $pattern), concat('$1',$replacement)) } ; (functx:get-matches( 'abc123def', '\d+')): expected '123 ' to equal ' 123 '
functx-functx-get-matches-2,AssertionError: declare namespace functx = "http://www.example.com/"; (:~ : Splits a string into matching and non-matching regions : : @author Priscilla Walmsley, Datypic : @version 1.0 : @see http://www.xqueryfunctions.com/xq/functx_get-matches-and-non-matches.html : @param $string the string to split : @param $regex the pattern :) declare function functx:get-matches-and-non-matches ( $string as xs:string? , $regex as xs:string ) as element()* { let $iomf := functx:index-of-match-first($string, $regex) return if (empty($iomf)) then {$string} else if ($iomf > 1) then ({substring($string,1,$iomf - 1)}, functx:get-matches-and-non-matches( substring($string,$iomf),$regex)) else let $length := string-length($string) - string-length(functx:replace-first($string, $regex,'')) return ({substring($string,1,$length)}, if (string-length($string) > $length) then functx:get-matches-and-non-matches( substring($string,$length + 1),$regex) else ()) } ; (:~ : Return the matching regions of a string : : @author Priscilla Walmsley, Datypic : @version 1.0 : @see http://www.xqueryfunctions.com/xq/functx_get-matches.html : @param $string the string to split : @param $regex the pattern :) declare function functx:get-matches ( $string as xs:string? , $regex as xs:string ) as xs:string* { functx:get-matches-and-non-matches($string,$regex)/ string(self::match) } ; (:~ : The first position of a matching substring : : @author Priscilla Walmsley, Datypic : @version 1.0 : @see http://www.xqueryfunctions.com/xq/functx_index-of-match-first.html : @param $arg the string : @param $pattern the pattern to match :) declare function functx:index-of-match-first ( $arg as xs:string? , $pattern as xs:string ) as xs:integer? { if (matches($arg,$pattern)) then string-length(tokenize($arg, $pattern)[1]) + 1 else () } ; (:~ : Replaces the first match of a pattern : : @author Priscilla Walmsley, Datypic : @version 1.0 : @see http://www.xqueryfunctions.com/xq/functx_replace-first.html : @param $arg the entire string to change : @param $pattern the pattern of characters to replace : @param $replacement the replacement string :) declare function functx:replace-first ( $arg as xs:string? , $pattern as xs:string , $replacement as xs:string ) as xs:string { replace($arg, concat('(^.*?)', $pattern), concat('$1',$replacement)) } ; (functx:get-matches( 'abc123def', '\d')): expected ' 1 3 2' to equal ' 1 2 3 '
functx-functx-get-matches-all,AssertionError: declare namespace functx = "http://www.example.com/"; (:~ : Splits a string into matching and non-matching regions : : @author Priscilla Walmsley, Datypic : @version 1.0 : @see http://www.xqueryfunctions.com/xq/functx_get-matches-and-non-matches.html : @param $string the string to split : @param $regex the pattern :) declare function functx:get-matches-and-non-matches ( $string as xs:string? , $regex as xs:string ) as element()* { let $iomf := functx:index-of-match-first($string, $regex) return if (empty($iomf)) then {$string} else if ($iomf > 1) then ({substring($string,1,$iomf - 1)}, functx:get-matches-and-non-matches( substring($string,$iomf),$regex)) else let $length := string-length($string) - string-length(functx:replace-first($string, $regex,'')) return ({substring($string,1,$length)}, if (string-length($string) > $length) then functx:get-matches-and-non-matches( substring($string,$length + 1),$regex) else ()) } ; (:~ : Return the matching regions of a string : : @author Priscilla Walmsley, Datypic : @version 1.0 : @see http://www.xqueryfunctions.com/xq/functx_get-matches.html : @param $string the string to split : @param $regex the pattern :) declare function functx:get-matches ( $string as xs:string? , $regex as xs:string ) as xs:string* { functx:get-matches-and-non-matches($string,$regex)/ string(self::match) } ; (:~ : The first position of a matching substring : : @author Priscilla Walmsley, Datypic : @version 1.0 : @see http://www.xqueryfunctions.com/xq/functx_index-of-match-first.html : @param $arg the string : @param $pattern the pattern to match :) declare function functx:index-of-match-first ( $arg as xs:string? , $pattern as xs:string ) as xs:integer? { if (matches($arg,$pattern)) then string-length(tokenize($arg, $pattern)[1]) + 1 else () } ; (:~ : Replaces the first match of a pattern : : @author Priscilla Walmsley, Datypic : @version 1.0 : @see http://www.xqueryfunctions.com/xq/functx_replace-first.html : @param $arg the entire string to change : @param $pattern the pattern of characters to replace : @param $replacement the replacement string :) declare function functx:replace-first ( $arg as xs:string? , $pattern as xs:string , $replacement as xs:string ) as xs:string { replace($arg, concat('(^.*?)', $pattern), concat('$1',$replacement)) } ; (functx:get-matches( 'abc123def', '\d+'), functx:get-matches( 'abc123def', '\d'), functx:get-matches( 'abc123def', '[a-z]{2}')): expected '123 1 3 2 ab de ' to equal ' 123 1 2 3 ab de '
-functx-functx-next-day-1,Error: Not implemented: adding durations to xs:date
-functx-functx-next-day-2,Error: Not implemented: adding durations to xs:date
-functx-functx-next-day-all,Error: Not implemented: adding durations to xs:date
-functx-functx-previous-day-1,Error: Not implemented: subtracting durations from xs:date
-functx-functx-previous-day-2,Error: Not implemented: subtracting durations from xs:date
-functx-functx-previous-day-all,Error: Not implemented: subtracting durations from xs:date
functx-functx-right-trim-all,Error: 1: deep-equal((declare namespace functx = "http://www.example.com/"; ^ 2: (:~ : Trims trailing whitespace : : @author Priscilla Walmsley, Datypic : @version 1.0 : @see http://www.xqueryfunctions.com/xq/functx_right-trim.html : @param $arg the string to trim :) 3: declare function functx:right-trim ( $arg as xs:string? ) as xs:string { replace($arg,'\s+$','') } ; Error: XPST0003: Failed to parse script. Expected end of input at <>:1:11 - 1:12
functx-functx-sort-as-numeric-all,Error: No selector counterpart for: computedDocumentConstructor.
functx-functx-sort-case-insensitive-all,Error: No selector counterpart for: computedDocumentConstructor.
diff --git a/test/specs/expressions/dataTypes/valueTypes/DateTime.tests.ts b/test/specs/expressions/dataTypes/valueTypes/DateTime.tests.ts
index c72b7f727..21a7c9343 100644
--- a/test/specs/expressions/dataTypes/valueTypes/DateTime.tests.ts
+++ b/test/specs/expressions/dataTypes/valueTypes/DateTime.tests.ts
@@ -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()', () => {
@@ -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'),
+ ),
+ );
});
});
diff --git a/test/specs/expressions/dataTypes/valueTypes/Duration.tests.ts b/test/specs/expressions/dataTypes/valueTypes/Duration.tests.ts
index 1c0c845ac..25b26b391 100644
--- a/test/specs/expressions/dataTypes/valueTypes/Duration.tests.ts
+++ b/test/specs/expressions/dataTypes/valueTypes/Duration.tests.ts
@@ -1,5 +1,6 @@
import * as chai from 'chai';
import Duration from 'fontoxpath/expressions/dataTypes/valueTypes/Duration';
+import YearMonthDuration from 'fontoxpath/expressions/dataTypes/valueTypes/YearMonthDuration';
describe('Data type: duration', () => {
describe('Duration.compare()', () => {
@@ -201,4 +202,30 @@ describe('Data type: duration', () => {
chai.assert.equal(duration2.compare(duration1), 0);
});
});
+ describe('Duration.negate()', () => {
+ it('negates a positiv YearMonthDuration duration', () => {
+ const duration = YearMonthDuration.fromString('P1Y2M');
+ const negated = duration.negate();
+ chai.assert.isFalse(negated.isPositive());
+ chai.assert.equal(negated.toString(), '-P1Y2M');
+ });
+ it('negates a negative YearMonthDuration duration', () => {
+ const duration = YearMonthDuration.fromString('-P1Y2M');
+ const negated = duration.negate();
+ chai.assert.isTrue(negated.isPositive());
+ chai.assert.equal(negated.toString(), 'P1Y2M');
+ });
+ it('negates a positiv DateTime duration', () => {
+ const duration = Duration.fromString('P1Y2M3DT4H5M6S');
+ const negated = duration.negate();
+ chai.assert.isFalse(negated.isPositive());
+ chai.assert.equal(negated.toString(), '-P1Y2M3DT4H5M6S');
+ });
+ it('negates a negative DateTime duration', () => {
+ const duration = Duration.fromString('-P1Y2M3DT4H5M6S');
+ const negated = duration.negate();
+ chai.assert.isTrue(negated.isPositive());
+ chai.assert.equal(negated.toString(), 'P1Y2M3DT4H5M6S');
+ });
+ });
});