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'); + }); + }); });