@@ -2,23 +2,43 @@ module RDF; class Literal
22 ##
33 # A date/time literal.
44 #
5- # @see http://www.w3.org/TR/xmlschema11-2/#dateTime#boolean
5+ # @see http://www.w3.org/TR/xmlschema11-2/#dateTime
66 # @since 0.2.1
77 class DateTime < Literal
88 DATATYPE = RDF ::URI ( "http://www.w3.org/2001/XMLSchema#dateTime" )
99 GRAMMAR = %r(\A (-?(?:\d {4}|[1-9]\d {4,})-\d {2}-\d {2}T\d {2}:\d {2}:\d {2}(?:\. \d +)?)((?:[\+ \- ]\d {2}:\d {2})|UTC|GMT|Z)?\Z ) . freeze
10- FORMAT = '%Y-%m-%dT%H:%M:%S.%L%:z' . freeze
10+ FORMAT = '%Y-%m-%dT%H:%M:%S.%L' . freeze
11+
12+ # Matches either -10:00 or -P1H0M forms
13+ ZONE_GRAMMAR = %r(\A
14+ (?:(?<si>[+-])(?<hr>\d {2}):(?:(?<mi>\d {2}))?)
15+ |(?:(?<si>-)?PT(?<hr>\d {1,2})H(?:(?<mi>\d {1,2})M)?)
16+ \z )x . freeze
1117
1218 ##
19+ # Internally, a `DateTime` is represented using a native `::DateTime`. If initialized from a `::Date`, there is no timezone component, If initialized from a `::DateTime`, the timezone is taken from that native object, otherwise, a timezone (or no timezone) is taken from the string representation having a matching `zzzzzz` component.
20+ #
1321 # @param [DateTime] value
1422 # @option options [String] :lexical (nil)
1523 def initialize ( value , datatype : nil , lexical : nil , **options )
1624 @datatype = RDF ::URI ( datatype || self . class . const_get ( :DATATYPE ) )
1725 @string = lexical || ( value if value . is_a? ( String ) )
1826 @object = case
19- when value . is_a? ( ::DateTime ) then value
20- when value . respond_to? ( :to_datetime ) then value . to_datetime
21- else ::DateTime . parse ( value . to_s )
27+ when value . is_a? ( ::DateTime )
28+ @zone = value . zone
29+ value
30+ when value . respond_to? ( :to_datetime )
31+ @zone = value . to_datetime . zone
32+ value . to_datetime
33+ else
34+ md = value . to_s . match ( GRAMMAR )
35+ _ , dt , tz = Array ( md )
36+ if tz
37+ @zone = tz == 'Z' ? '+00:00' : tz
38+ else
39+ @zone = nil # No timezone
40+ end
41+ ::DateTime . parse ( value . to_s )
2242 end rescue ::DateTime . new
2343 end
2444
@@ -29,12 +49,10 @@ def initialize(value, datatype: nil, lexical: nil, **options)
2949 # @return [RDF::Literal] `self`
3050 # @see http://www.w3.org/TR/xmlschema11-2/#dateTime
3151 def canonicalize!
32- if self . valid?
33- @string = if timezone?
34- @object . new_offset . new_offset . strftime ( FORMAT [ 0 ..-4 ] + 'Z' ) . sub ( '.000' , '' )
35- else
36- @object . strftime ( FORMAT [ 0 ..-4 ] ) . sub ( '.000' , '' )
37- end
52+ if self . valid? && @zone && @zone != '+00:00'
53+ adjust_to_timezone!
54+ else
55+ @string = nil
3856 end
3957 self
4058 end
@@ -43,18 +61,32 @@ def canonicalize!
4361 # Returns the timezone part of arg as a simple literal. Returns the empty string if there is no timezone.
4462 #
4563 # @return [RDF::Literal]
46- # @see http://www.w3.org/TR/sparql11-query/#func-tz
4764 def tz
48- zone = timezone? ? object . zone : ""
49- zone = "Z" if zone == "+00:00"
50- RDF ::Literal ( zone )
65+ RDF ::Literal ( @zone == "+00:00" ? 'Z' : @zone )
5166 end
5267
68+ ##
69+ # Does the literal representation include a timezone? Note that this is only possible if initialized using a string, or `:lexical` option.
70+ #
71+ # @return [Boolean]
72+ # @since 1.1.6
73+ def timezone?
74+ # Can only know there's a timezone from the string represntation
75+ md = to_s . match ( GRAMMAR )
76+ md && !!md [ 2 ]
77+ end
78+ alias_method :tz? , :timezone?
79+ alias_method :has_tz? , :timezone?
80+ alias_method :has_timezone? , :timezone?
81+
5382 ##
5483 # Returns the timezone part of arg as an xsd:dayTimeDuration, or `nil`
5584 # if lexical form of literal does not include a timezone.
5685 #
86+ # From [fn:timezone-from-date](https://www.w3.org/TR/xpath-functions/#func-timezone-from-date).
87+ #
5788 # @return [RDF::Literal]
89+ # @see https://www.w3.org/TR/xpath-functions/#func-timezone-from-date
5890 def timezone
5991 if tz == 'Z'
6092 RDF ::Literal ( "PT0S" , datatype : RDF ::URI ( "http://www.w3.org/2001/XMLSchema#dayTimeDuration" ) )
@@ -86,33 +118,20 @@ def valid?
86118 # @return [Boolean]
87119 # @since 1.1.6
88120 def milliseconds?
89- self . format ( "%L" ) . to_i > 0
121+ object . strftime ( "%L" ) . to_i > 0
90122 end
91123 alias_method :has_milliseconds? , :milliseconds?
92124 alias_method :has_ms? , :milliseconds?
93125 alias_method :ms? , :milliseconds?
94126
95- ##
96- # Does the literal representation include a timezone? Note that this is only possible if initialized using a string, or `:lexical` option.
97- #
98- # @return [Boolean]
99- # @since 1.1.6
100- def timezone?
101- md = self . to_s . match ( GRAMMAR )
102- md && !!md [ 2 ]
103- end
104- alias_method :tz? , :timezone?
105- alias_method :has_tz? , :timezone?
106- alias_method :has_timezone? , :timezone?
107-
108127 ##
109128 # Returns the `timezone` of the literal. If the
110129 ##
111130 # Returns the value as a string.
112131 #
113132 # @return [String]
114133 def to_s
115- @string || @object . strftime ( FORMAT ) . sub ( "+00:00" , 'Z' ) . sub ( '. 000', '' )
134+ @string || ( @object . strftime ( FORMAT ) . sub ( '. 000', '' ) + self . tz )
116135 end
117136
118137 ##
@@ -123,31 +142,153 @@ def to_s
123142 def humanize ( lang = :en )
124143 d = object . strftime ( "%r on %A, %d %B %Y" )
125144 if timezone?
126- zone = if self . tz == 'Z'
127- "UTC"
128- else
129- self . tz
130- end
131- d . sub! ( " on " , " #{ zone } on " )
145+ z = @zone == '+00:00' ? "UTC" : @zone
146+ d . sub! ( " on " , " #{ z } on " )
132147 end
133148 d
134149 end
135150
151+ ##
152+ # Adjust the timezone.
153+ #
154+ # From [fn:adjust-dateTime-to-timezone](https://www.w3.org/TR/xpath-functions/#func-adjust-dateTime-to-timezone)
155+ #
156+ # @overload adjust_to_timezone!
157+ # Adjusts the timezone to UTC.
158+ #
159+ # @return [DateTime] `self`
160+ # @raise [RangeError] if `zone < -14*60` or `zone > 14*60`
161+ # @overload adjust_to_timezone!(zone)
162+ # If `zone` is nil, then the timzeone component is removed.
163+ #
164+ # Otherwise, the timezone is set based on the difference between the current timezone offset (if any) and `zone`.
165+ #
166+ # @param [String] zone (nil) In the form of {ZONE_FORMAT}
167+ # @return [DateTime] `self`
168+ # @raise [RangeError] if `zone < -14*60` or `zone > 14*60`
169+ # @see https://www.w3.org/TR/xpath-functions/#func-adjust-dateTime-to-timezone
170+ def adjust_to_timezone! ( *args )
171+ zone = args . empty? ? '+00:00' : args . first
172+ if zone . nil?
173+ # Remove timezone component
174+ @object = ::DateTime . parse ( @object . strftime ( FORMAT ) )
175+ @zone = nil
176+ else
177+ md = zone . match ( Literal ::DateTime ::ZONE_GRAMMAR ) if zone
178+ raise ArgumentError ,
179+ "expected #{ zone . inspect } to be a xsd:dayTimeDuration or +/-HH:MM" unless md
180+
181+ # Adjust to zone
182+ si , hr , mi = md [ :si ] , md [ :hr ] , md [ :mi ]
183+ si ||= '+'
184+ offset = hr . to_i * 60 + mi . to_i
185+ raise ArgumentError ,
186+ "Zone adjustment of #{ zone } out of range" if
187+ md . nil? || offset > 14 *60
188+
189+ new_zone = "%s%.2d:%.2d" % [ si , hr . to_i , mi . to_i ]
190+ dt = @zone . nil? ? @object : @object . new_offset ( new_zone )
191+ @object = ::DateTime . parse ( dt . strftime ( FORMAT + new_zone ) )
192+ @zone = new_zone
193+ end
194+ @string = nil
195+ self
196+ end
197+
198+ ##
199+ # Functional version of `#adjust_to_timezone!`.
200+ #
201+ # @overload adjust_to_timezone
202+ # @param (see #adjust_to_timezone!)
203+ # @return [DateTime]
204+ # @raise (see #adjust_to_timezone!)
205+ # @overload adjust_to_timezone(zone) (see #adjust_to_timezone!)
206+ # @return [DateTime]
207+ # @raise (see #adjust_to_timezone!)
208+ def adjust_to_timezone ( *args )
209+ self . dup . adjust_to_timezone! ( *args )
210+ end
211+
136212 ##
137213 # Equal compares as DateTime objects
214+ #
215+ # From the XQuery function [op:dateTime-equal](https://www.w3.org/TR/xpath-functions/#func-dateTime-equal).
216+ #
217+ # @param [Literal::Date, Literal] other
218+ # @return [Boolean]
219+ # @see https://www.w3.org/TR/xpath-functions/#func-dateTime-equal
138220 def ==( other )
139221 # If lexically invalid, use regular literal testing
140- return super unless self . valid?
222+ return super unless self . valid? && ( ! other . respond_to? ( :valid? ) || other . valid? )
141223
142224 case other
143225 when Literal ::DateTime
144- return super unless other . valid?
145226 self . object == other . object
146227 when Literal ::Time , Literal ::Date
147228 false
148229 else
149230 super
150231 end
151232 end
233+
234+ ##
235+ # Compares `self` to `other` for sorting purposes (with type check).
236+ #
237+ # @param [Object] other
238+ # @return [Integer] `-1`, `0`, or `1`
239+ def <=>( other )
240+ # If lexically invalid, use regular literal testing
241+ return super unless self . valid? && ( !other . respond_to? ( :valid? ) || other . valid? )
242+ return super unless other . is_a? ( DateTime )
243+ @object <=> other . object
244+ end
245+
246+ # Years
247+ #
248+ # From the XQuery function [fn:year-from-dateTime](https://www.w3.org/TR/xpath-functions/#func-year-from-dateTime).
249+ #
250+ # @return [Integer]
251+ # @see https://www.w3.org/TR/xpath-functions/#func-year-from-dateTime
252+ def year ; Integer . new ( object . year ) ; end
253+
254+ # Months
255+ #
256+ # From the XQuery function [fn:month-from-dateTime](https://www.w3.org/TR/xpath-functions/#func-month-from-dateTime).
257+ #
258+ # @return [Integer]
259+ # @see https://www.w3.org/TR/xpath-functions/#func-month-from-dateTime
260+ def month ; Integer . new ( object . month ) ; end
261+
262+ # Days
263+ #
264+ # From the XQuery function [fn:day-from-dateTime](https://www.w3.org/TR/xpath-functions/#func-day-from-dateTime).
265+ #
266+ # @return [Integer]
267+ # @see https://www.w3.org/TR/xpath-functions/#func-day-from-dateTime
268+ def day ; Integer . new ( object . day ) ; end
269+
270+ # Hours
271+ #
272+ # From the XQuery function [fn:hours-from-dateTime](https://www.w3.org/TR/xpath-functions/#func-hours-from-dateTime).
273+ #
274+ # @return [Integer]
275+ # @see https://www.w3.org/TR/xpath-functions/#func-hours-from-dateTime
276+ def hours ; Integer . new ( object . hour ) ; end
277+
278+ # Minutes
279+ #
280+ # From the XQuery function [fn:minutes-from-dateTime](https://www.w3.org/TR/xpath-functions/#func-minutes-from-dateTime).
281+ #
282+ # @return [Integer]
283+ # @see https://www.w3.org/TR/xpath-functions/#func-minutes-from-dateTime
284+ def minutes ; Integer . new ( object . min ) ; end
285+
286+ # Seconds
287+ #
288+ # From the XQuery function [fn:seconds-from-dateTime](https://www.w3.org/TR/xpath-functions/#func-seconds-from-dateTime).
289+ #
290+ # @return [Decimal]
291+ # @see https://www.w3.org/TR/xpath-functions/#func-seconds-from-dateTime
292+ def seconds ; Decimal . new ( object . strftime ( "%S.%L" ) ) ; end
152293 end # DateTime
153294end ; end # RDF::Literal
0 commit comments