Skip to content

Commit c9a7677

Browse files
committed
Add and subtract temporals with each other and durations, per XPath/XQuery functions spec.
1 parent cd1da2f commit c9a7677

2 files changed

Lines changed: 283 additions & 2 deletions

File tree

lib/rdf/model/literal/temporal.rb

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ def to_s
146146
#
147147
# Otherwise, the timezone is set based on the difference between the current timezone offset (if any) and `zone`.
148148
#
149-
# @param [String] zone (nil) In the form of {ZONE_FORMAT}
149+
# @param [String] zone (nil) In the form of {ZONE_GRAMMAR}.
150150
# @return [Temporal] `self`
151151
# @raise [RangeError] if `zone < -14*60` or `zone > 14*60`
152152
# @see https://www.w3.org/TR/xpath-functions/#func-adjust-dateTime-to-timezone
@@ -192,6 +192,73 @@ def adjust_to_timezone(*args)
192192
self.dup.adjust_to_timezone!(*args)
193193
end
194194

195+
##
196+
# Add a Duration to a Temporal.
197+
#
198+
# For YearMonthDuration, turns duration into months and adds to internal DateTime object.
199+
#
200+
# For DayTimeDuration, turns duration into rational days, and adds to internal DateTime object.
201+
#
202+
# @note This depends on the parameter responding to `#to_i` or `#to_r`, which for Duration types, is implemented in the rdf-xsd gem.
203+
#
204+
# @param [YearMonthDuration, DayTimeDuration] other
205+
# @return [Temporal]
206+
# @see https://www.w3.org/TR/xpath-functions/#func-add-yearMonthDuration-to-dateTime
207+
# @see https://www.w3.org/TR/xpath-functions/#func-add-dayTimeDuration-to-dateTime
208+
def +(other)
209+
new_dt = case other
210+
when YearMonthDuration
211+
@object >> other.to_i
212+
when DayTimeDuration
213+
@object + other.to_r
214+
else
215+
return super
216+
end
217+
218+
dt = new_dt.strftime(self.class.const_get(:FORMAT)) + tz
219+
self.class.new(dt)
220+
rescue NoMethodError => e
221+
raise "Consider including the rdf-xsd class for method implementaions: #{e.message}"
222+
end
223+
224+
##
225+
# Subtract times or durations from a temporal.
226+
#
227+
# @overload +(other)
228+
# For YearMonthDuration, turns duration into months and subtracts from internal DateTime object resulting in a new {Temporal} object.
229+
#
230+
# For DayTimeDuration, turns duration into rational days, and subtracts from internal DateTime object resulting in a new {Temporal} object.
231+
#
232+
# For Temporal, subtracts the two moments resulting in a `xsd:dayTimeDuration`.
233+
#
234+
# @param [YearMonthDuration, DayTimeDurationm, Temporal] other
235+
# @return [Temporal, DayTimeDuration]
236+
# @note This depends on the parameter responding to `#to_i` or `#to_r`, which for Duration types, is implemented in the rdf-xsd gem.
237+
# @see https://www.w3.org/TR/xpath-functions/#func-subtract-yearMonthDuration-from-dateTime
238+
# @see https://www.w3.org/TR/xpath-functions/#func-subtract-dayTimeDuration-from-dateTime
239+
# @see https://www.w3.org/TR/xpath-functions/#func-subtract-dateTimes
240+
def -(other)
241+
new_dt = case other
242+
when YearMonthDuration
243+
@object << other.to_i
244+
when DayTimeDuration
245+
@object - other.to_r
246+
when Temporal
247+
@object - other.object
248+
else
249+
return super
250+
end
251+
252+
if new_dt.is_a?(Rational)
253+
RDF::Literal(new_dt, datatype: RDF::XSD.dayTimeDuration)
254+
else
255+
dt = new_dt.strftime(self.class.const_get(:FORMAT)) + tz
256+
self.class.new(dt)
257+
end
258+
rescue NoMethodError => e
259+
raise "Consider including the rdf-xsd class for method implementaions: #{e.message}"
260+
end
261+
195262
# Years
196263
#
197264
# From the XQuery function [fn:year-from-dateTime](https://www.w3.org/TR/xpath-functions/#func-year-from-dateTime).

spec/model_literal_spec.rb

Lines changed: 215 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -994,8 +994,88 @@ def self.literals(*selector)
994994
end
995995
end
996996
end
997-
end
998997

998+
describe "#+" do
999+
context "xsd:dayTimeDuration" do
1000+
{
1001+
["2000-10-30T11:12:00", "P3DT1H15M"] => "2000-11-02T12:27:00",
1002+
["2000-10-30T11:12:00Z", "P3DT1H15M"] => "2000-11-02T12:27:00Z",
1003+
["2000-10-30T11:12:00-08:00", "P3DT1H15M"] => "2000-11-02T12:27:00-08:00",
1004+
["2000-10-30T11:12:00", "-P3D"] => "2000-10-27T11:12:00",
1005+
}.each do |(t, d), res|
1006+
it "#{t} + #{d} == #{res}" do
1007+
t1 = described_class.new(t)
1008+
dur = RDF::Literal(d, datatype: RDF::XSD.dayTimeDuration)
1009+
expect(t1 + dur).to eq described_class.new(res)
1010+
end
1011+
end
1012+
end
1013+
1014+
context "xsd:yearMonthDuration" do
1015+
{
1016+
["2000-10-30T11:12:00", "P1Y2M"] => "2001-12-30T11:12:00",
1017+
["2000-10-30T11:12:00Z", "P1Y2M"] => "2001-12-30T11:12:00Z",
1018+
["2000-10-30T11:12:00-08:00", "P1Y2M"] => "2001-12-30T11:12:00-08:00",
1019+
["2000-10-30T11:12:00", "-P1Y2M"] => "1999-08-30T11:12:00",
1020+
}.each do |(t, d), res|
1021+
it "#{t} + #{d} == #{res}" do
1022+
t1 = described_class.new(t)
1023+
dur = RDF::Literal(d, datatype: RDF::XSD.yearMonthDuration)
1024+
expect(t1 + dur).to eq described_class.new(res)
1025+
end
1026+
end
1027+
end
1028+
end
1029+
1030+
describe "#-" do
1031+
context "xsd:dayTimeDuration" do
1032+
{
1033+
["2000-10-30T11:12:00", "P3DT1H15M"] => "2000-10-27T09:57:00",
1034+
["2000-10-30T11:12:00Z", "P3DT1H15M"] => "2000-10-27T09:57:00Z",
1035+
["2000-10-30T11:12:00-08:00", "P3DT1H15M"] => "2000-10-27T09:57:00-08:00",
1036+
["2000-10-30T11:12:00", "-P3D"] => "2000-11-02T11:12:00",
1037+
}.each do |(t, d), res|
1038+
it "#{t} - #{d} == #{res}" do
1039+
t1 = described_class.new(t)
1040+
dur = RDF::Literal(d, datatype: RDF::XSD.dayTimeDuration)
1041+
expect(t1 - dur).to eq described_class.new(res)
1042+
end
1043+
end
1044+
end
1045+
1046+
context "xsd:yearMonthDuration" do
1047+
{
1048+
["2000-10-30T11:12:00", "P1Y2M"] => "1999-08-30T11:12:00",
1049+
["2000-10-30T11:12:00Z", "P1Y2M"] => "1999-08-30T11:12:00Z",
1050+
["2000-10-30T11:12:00-08:00", "P1Y2M"] => "1999-08-30T11:12:00-08:00",
1051+
["2000-10-30T11:12:00", "-P1Y2M"] => "2001-12-30T11:12:00",
1052+
}.each do |(t, d), res|
1053+
it "#{t} - #{d} == #{res}" do
1054+
t1 = described_class.new(t)
1055+
dur = RDF::Literal(d, datatype: RDF::XSD.yearMonthDuration)
1056+
expect(t1 - dur).to eq described_class.new(res)
1057+
end
1058+
end
1059+
end
1060+
1061+
context "xsd:dateTime" do
1062+
{
1063+
["2000-10-30T11:12:00", "2000-08-30T11:12:00"] => "P61D",
1064+
["2000-10-30T11:12:00Z", "2000-08-30T11:12:00Z"] => "P61D",
1065+
["2000-10-30T11:12:00-08:00", "2000-08-30T11:12:00-08:00"] => "P61D",
1066+
["2000-10-30T11:12:00", "2000-12-30T11:12:00"] => "-P61D",
1067+
["2000-10-30T06:12:00-05:00", "1999-11-28T09:00:00Z"] => "P337DT2H12M",
1068+
}.each do |(t1, t2), res|
1069+
it "#{t1} - #{t2} == #{res}" do
1070+
t1 = described_class.new(t1)
1071+
t2 = described_class.new(t2)
1072+
res = RDF::Literal(res, datatype: RDF::XSD.dayTimeDuration)
1073+
expect(t1 - t2).to eq res
1074+
end
1075+
end
1076+
end
1077+
end
1078+
end
9991079

10001080
describe RDF::Literal::Date do
10011081
it_behaves_like 'RDF::Literal with datatype and grammar', "2010-01-01T00:00:00Z", RDF::XSD.date
@@ -1188,6 +1268,87 @@ def self.literals(*selector)
11881268
end
11891269
end
11901270
end
1271+
1272+
describe "#+" do
1273+
context "xsd:dayTimeDuration" do
1274+
{
1275+
["2000-10-30", "P3DT1H15M"] => "2000-11-02",
1276+
["2000-10-30Z", "P3DT1H15M"] => "2000-11-02Z",
1277+
["2000-10-30-08:00", "P3DT1H15M"] => "2000-11-02-08:00",
1278+
["2000-10-30", "-P3D"] => "2000-10-27",
1279+
["2004-10-30Z", "P2DT2H30M0S"] => "2004-11-01Z",
1280+
}.each do |(t, d), res|
1281+
it "#{t} + #{d} == #{res}" do
1282+
t1 = described_class.new(t)
1283+
dur = RDF::Literal(d, datatype: RDF::XSD.dayTimeDuration)
1284+
expect(t1 + dur).to eq described_class.new(res)
1285+
end
1286+
end
1287+
end
1288+
1289+
context "xsd:yearMonthDuration" do
1290+
{
1291+
["2000-10-30", "P1Y2M"] => "2001-12-30",
1292+
["2000-10-30Z", "P1Y2M"] => "2001-12-30Z",
1293+
["2000-10-30-08:00", "P1Y2M"] => "2001-12-30-08:00",
1294+
["2000-10-30", "-P1Y2M"] => "1999-08-30",
1295+
}.each do |(t, d), res|
1296+
it "#{t} + #{d} == #{res}" do
1297+
t1 = described_class.new(t)
1298+
dur = RDF::Literal(d, datatype: RDF::XSD.yearMonthDuration)
1299+
expect(t1 + dur).to eq described_class.new(res)
1300+
end
1301+
end
1302+
end
1303+
1304+
describe "#-" do
1305+
context "xsd:dayTimeDuration" do
1306+
{
1307+
["2000-10-30", "P3DT1H15M"] => "2000-10-26",
1308+
["2000-10-30Z", "P3DT1H15M"] => "2000-10-26Z",
1309+
["2000-10-30-08:00", "P3DT1H15M"] => "2000-10-26-08:00",
1310+
}.each do |(t, d), res|
1311+
it "#{t} - #{d} == #{res}" do
1312+
t1 = described_class.new(t)
1313+
dur = RDF::Literal(d, datatype: RDF::XSD.dayTimeDuration)
1314+
expect(t1 - dur).to eq described_class.new(res)
1315+
end
1316+
end
1317+
end
1318+
1319+
context "xsd:yearMonthDuration" do
1320+
{
1321+
["2000-10-30", "P1Y2M"] => "1999-08-30",
1322+
["2000-02-29Z", "P1Y"] => "1999-02-28Z",
1323+
["2000-10-31-05:00", "P1Y1M"] => "1999-09-30-05:00",
1324+
["2000-10-30", "-P1Y2M"] => "2001-12-30",
1325+
}.each do |(t, d), res|
1326+
it "#{t} - #{d} == #{res}" do
1327+
t1 = described_class.new(t)
1328+
dur = RDF::Literal(d, datatype: RDF::XSD.yearMonthDuration)
1329+
expect(t1 - dur).to eq described_class.new(res)
1330+
end
1331+
end
1332+
end
1333+
1334+
context "xsd:date" do
1335+
{
1336+
["2000-10-30", "2000-08-30"] => "P61D",
1337+
["2000-10-30Z", "2000-08-30Z"] => "P61D",
1338+
["2000-10-30-08:00", "2000-08-30-08:00"] => "P61D",
1339+
["2000-10-30", "2000-12-30"] => "-P61D",
1340+
["2000-10-30-05:00", "1999-11-28Z"] => "P337DT5H",
1341+
}.each do |(t1, t2), res|
1342+
it "#{t1} - #{t2} == #{res}" do
1343+
t1 = described_class.new(t1)
1344+
t2 = described_class.new(t2)
1345+
res = RDF::Literal(res, datatype: RDF::XSD.dayTimeDuration)
1346+
expect(t1 - t2).to eq res
1347+
end
1348+
end
1349+
end
1350+
end
1351+
end
11911352
end
11921353

11931354
describe RDF::Literal::Time do
@@ -1389,6 +1550,59 @@ def self.literals(*selector)
13891550
end
13901551
end
13911552
end
1553+
1554+
describe "#+" do
1555+
context "xsd:dayTimeDuration" do
1556+
{
1557+
["11:12:00", "P3DT1H15M"] => "12:27:00",
1558+
["11:12:00Z", "P3DT1H15M"] => "12:27:00Z",
1559+
["11:12:00-08:00", "P3DT1H15M"] => "12:27:00-08:00",
1560+
["11:12:00", "-P3D"] => "11:12:00",
1561+
}.each do |(t, d), res|
1562+
it "#{t} + #{d} == #{res}" do
1563+
t1 = described_class.new(t)
1564+
dur = RDF::Literal(d, datatype: RDF::XSD.dayTimeDuration)
1565+
expect(t1 + dur).to eq described_class.new(res)
1566+
end
1567+
end
1568+
end
1569+
end
1570+
1571+
describe "#-" do
1572+
context "xsd:dayTimeDuration" do
1573+
{
1574+
["11:12:00", "P3DT1H15M"] => "09:57:00",
1575+
["11:12:00Z", "P3DT1H15M"] => "09:57:00Z",
1576+
["11:12:00-08:00", "P3DT1H15M"] => "09:57:00-08:00",
1577+
["11:12:00", "-PT3H"] => "14:12:00",
1578+
["08:20:00-05:00", "P23DT10H10M"] => "22:10:00-05:00",
1579+
}.each do |(t, d), res|
1580+
it "#{t} - #{d} == #{res}" do
1581+
t1 = described_class.new(t)
1582+
dur = RDF::Literal(d, datatype: RDF::XSD.dayTimeDuration)
1583+
expect(t1 - dur).to eq described_class.new(res)
1584+
end
1585+
end
1586+
end
1587+
1588+
context "xsd:time" do
1589+
{
1590+
["11:12:00", "09:57:00"] => "PT1H15M",
1591+
["11:12:00Z", "04:00:00-05:00"] => "PT2H12M",
1592+
["11:00:00-05:00", "21:30:00+05:30"] => "PT0S",
1593+
["17:00:00-06:00", "08:00:00+09:00"] => "P1D",
1594+
["24:00:00", "23:59:59"] => "-PT23H59M59S",
1595+
["11:12:00", "14:12:00"] => "-PT3H",
1596+
}.each do |(t1, t2), res|
1597+
it "#{t1} - #{t2} == #{res}" do
1598+
t1 = described_class.new(t1)
1599+
t2 = described_class.new(t2)
1600+
res = RDF::Literal(res, datatype: RDF::XSD.dayTimeDuration)
1601+
expect(t1 - t2).to eq res
1602+
end
1603+
end
1604+
end
1605+
end
13921606
end
13931607

13941608
describe RDF::Literal::Numeric do

0 commit comments

Comments
 (0)