@@ -7,19 +7,44 @@ module RDF; class Literal
77 class Date < Literal
88 DATATYPE = RDF ::URI ( "http://www.w3.org/2001/XMLSchema#date" )
99 GRAMMAR = %r(\A (-?\d {4}-\d {2}-\d {2})((?:[\+ \- ]\d {2}:\d {2})|UTC|GMT|Z)?\Z ) . freeze
10+
11+ # Matches either -10:00 or -P1H0M forms
12+ ZONE_GRAMMAR = %r(\A
13+ (?:(?<si>[+-])(?<hr>\d {2}):(?:(?<mi>\d {2}))?)
14+ |(?:(?<si>-)?PT(?<hr>\d {1,2})H(?:(?<mi>\d {1,2})M)?)
15+ \z )x . freeze
1016 FORMAT = '%Y-%m-%d' . freeze
1117
1218 ##
13- # @param [String, Date, #to_date] value
19+ # Internally, a `Date` is represented using a native `::DateTime` object at noon. 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+ #
21+ # @note If initialized using the `#to_datetime` method, time component is unchanged. Otherewise, it is set to 00:00 (midnight).
22+ #
23+ # @param [String, Date, #to_datetime] value
1424 # @param (see Literal#initialize)
1525 def initialize ( value , datatype : nil , lexical : nil , **options )
1626 @datatype = RDF ::URI ( datatype || self . class . const_get ( :DATATYPE ) )
1727 @string = lexical || ( value if value . is_a? ( String ) )
1828 @object = case
19- when value . is_a? ( ::Date ) then value
20- when value . respond_to? ( :to_date ) then value . to_date
21- else ::Date . parse ( value . to_s )
22- end rescue ::Date . new
29+ when value . class == ::Date
30+ @zone = nil
31+ # Use noon as midpoint of the interval
32+ ::DateTime . parse ( value . strftime ( '%FT00:00:00' ) )
33+ when value . respond_to? ( :to_datetime )
34+ dt = value . to_datetime
35+ @zone = dt . zone
36+ dt
37+ else
38+ md = value . to_s . match ( GRAMMAR )
39+ _ , dt , tz = Array ( md )
40+ if tz
41+ @zone = tz == 'Z' ? '+00:00' : tz
42+ else
43+ @zone = nil # No timezone
44+ end
45+ # Use noon as midpoint of the interval
46+ ::DateTime . parse ( "#{ dt } T00:00:00#{ @zone } " )
47+ end rescue ::DateTime . new
2348 end
2449
2550 ##
@@ -30,7 +55,11 @@ def initialize(value, datatype: nil, lexical: nil, **options)
3055 # @return [RDF::Literal] `self`
3156 # @see http://www.w3.org/TR/xmlschema11-2/#date
3257 def canonicalize!
33- @string = @object . strftime ( FORMAT ) + self . tz . to_s if self . valid?
58+ if self . valid? && @zone && @zone != '+00:00'
59+ adjust_to_timezone!
60+ else
61+ @string = nil
62+ end
3463 self
3564 end
3665
@@ -46,25 +75,12 @@ def valid?
4675 super && object && value !~ %r(\A 0000)
4776 end
4877
49- ##
50- # Does the literal representation include a timezone? Note that this is only possible if initialized using a string, or `:lexical` option.
51- #
52- # @return [Boolean]
53- # @since 1.1.6
54- def timezone?
55- md = self . to_s . match ( GRAMMAR )
56- md && !!md [ 2 ]
57- end
58- alias_method :tz? , :timezone?
59- alias_method :has_tz? , :timezone?
60- alias_method :has_timezone? , :timezone?
61-
6278 ##
6379 # Returns the value as a string.
6480 #
6581 # @return [String]
6682 def to_s
67- @string || @object . strftime ( FORMAT )
83+ @string || ( @object . strftime ( FORMAT ) + self . tz )
6884 end
6985
7086 ##
@@ -75,32 +91,131 @@ def to_s
7591 def humanize ( lang = :en )
7692 d = object . strftime ( "%A, %d %B %Y" )
7793 if timezone?
78- d += if self . tz == 'Z '
94+ d += if @zone == '+00:00 '
7995 " UTC"
8096 else
81- " #{ self . tz } "
97+ " #{ @zone } "
8298 end
8399 end
84100 d
85101 end
86102
103+ ##
104+ # Does the literal representation include a timezone? Note that this is only possible if initialized using a string, or `:lexical` option.
105+ #
106+ # @return [Boolean]
107+ # @since 1.1.6
108+ def timezone?
109+ !@zone . nil?
110+ end
111+ alias_method :tz? , :timezone?
112+ alias_method :has_tz? , :timezone?
113+ alias_method :has_timezone? , :timezone?
114+
115+ ##
116+ # Adjust the timezone.
117+ #
118+ # From [fn:adjust-date-to-timezone](https://www.w3.org/TR/xpath-functions/#func-adjust-date-to-timezone)
119+ #
120+ # @overload adjust_to_timezone!
121+ # Adjusts the timezone to UTC.
122+ #
123+ # @return [Date] `self`
124+ # @raise [RangeError] if `zone < -14*60` or `zone > 14*60`
125+ # @overload adjust_to_timezone!(zone)
126+ # If `zone` is nil, then the timzeone component is removed.
127+ #
128+ # Otherwise, the timezone is set based on the difference between the current timezone offset (if any) and `zone`.
129+ #
130+ # @param [String] zone (nil) In the form of {ZONE_FORMAT}
131+ # @return [Date] `self`
132+ # @raise [RangeError] if `zone < -14*60` or `zone > 14*60`
133+ # @see https://www.w3.org/TR/xpath-functions/#func-adjust-date-to-timezone
134+ def adjust_to_timezone! ( *args )
135+ zone = args . first
136+ md = zone . match ( ZONE_GRAMMAR ) if zone
137+ raise ArgumentError ,
138+ "expected #{ zone . inspect } to be a xsd:dayTimeDuration or +/-HH:MM" if
139+ zone && !md
140+ if args . empty?
141+ @object = ::DateTime . parse ( @object . strftime ( '%F' ) )
142+ @zone = '+00:00'
143+ elsif zone . nil?
144+ # Remove timezone component
145+ @object = ::DateTime . parse ( @object . strftime ( '%F' ) )
146+ @zone = nil
147+ else
148+ # Adjust to
149+ si , hr , mi = md [ :si ] , md [ :hr ] , md [ :mi ]
150+ si ||= '+'
151+ offset = hr . to_i * 60 + mi . to_i
152+ raise ArgumentError ,
153+ "Zone adjustment of #{ zone } out of range" if
154+ md . nil? || offset > 14 *60
155+
156+ new_zone = "%s%.2d:%.2d" % [ si , hr . to_i , mi . to_i ]
157+ dt = @zone . nil? ? @object : @object . new_offset ( new_zone )
158+ @object = ::DateTime . parse ( dt . strftime ( "%FT00:00:00#{ new_zone } " ) )
159+ @zone = new_zone
160+ end
161+ @string = nil
162+ self
163+ end
164+
165+ ##
166+ # Functional version of `#adjust_to_timezone!`.
167+ #
168+ # @overload adjust_to_timezone
169+ # @param (see #adjust_to_timezone!)
170+ # @return [Date]
171+ # @raise (see #adjust_to_timezone!)
172+ # @overload adjust_to_timezone(zone) (see #adjust_to_timezone!)
173+ # @return [Date]
174+ # @raise (see #adjust_to_timezone!)
175+ def adjust_to_timezone ( *args )
176+ self . dup . adjust_to_timezone! ( *args )
177+ end
178+
87179 ##
88180 # Returns the timezone part of arg as a simple literal. Returns the empty string if there is no timezone.
89181 #
90182 # @return [RDF::Literal]
91183 # @since 1.1.6
92184 def tz
93- md = self . to_s . match ( GRAMMAR )
94- zone = md [ 2 ] . to_s
95- zone = "Z" if zone == "+00:00"
96- RDF ::Literal ( zone )
185+ RDF ::Literal ( @zone == "+00:00" ? 'Z' : @zone )
97186 end
98187
188+ ##
189+ # Returns the timezone part of arg as an xsd:dayTimeDuration, or `nil`
190+ # if lexical form of literal does not include a timezone.
191+ #
192+ # From [fn:timezone-from-dateTime](https://www.w3.org/TR/xpath-functions/#func-timezone-from-dateTime).
193+ #
194+ # @return [RDF::Literal]
195+ # @see https://www.w3.org/TR/xpath-functions/#func-timezone-from-dateTime
196+ def timezone
197+ if @zone
198+ md = @zone . match ( ZONE_GRAMMAR )
199+ si , hr , mi = md [ :si ] , md [ :hr ] . to_i , md [ :mi ] . to_i
200+ si = nil unless si == "-"
201+ res = "#{ si } PT#{ hr } H#{ "#{ mi } M" if mi > 0 } "
202+ RDF ::Literal ( res , datatype : RDF ::URI ( "http://www.w3.org/2001/XMLSchema#dayTimeDuration" ) )
203+ end
204+ end
205+
206+ ##
207+ # Updates the date to a new timezone, or no timezone.
99208 ##
100209 # Equal compares as Date objects
210+ #
211+ # From the XQuery function [op:date-equal](https://www.w3.org/TR/xpath-functions/#func-date-equal).
212+ #
213+ # @param [Date, Literal] other
214+ # @return [Boolean]
215+ # @see https://www.w3.org/TR/xpath-functions/#func-date-equal
101216 def ==( other )
102217 # If lexically invalid, use regular literal testing
103- return super unless self . valid?
218+ return super unless self . valid? && ( ! other . respond_to? ( :valid? ) || other . valid? )
104219
105220 case other
106221 when Literal ::Date
@@ -112,5 +227,41 @@ def ==(other)
112227 super
113228 end
114229 end
230+
231+ ##
232+ # Compares `self` to `other` for sorting purposes (with type check).
233+ #
234+ # @param [Object] other
235+ # @return [Integer] `-1`, `0`, or `1`
236+ def <=>( other )
237+ # If lexically invalid, use regular literal testing
238+ return super unless self . valid? && ( !other . respond_to? ( :valid? ) || other . valid? )
239+ return super unless other . is_a? ( Date )
240+ @object <=> other . object
241+ end
242+
243+ # Years
244+ #
245+ # From the XQuery function [fn:year-from-date](https://www.w3.org/TR/xpath-functions/#func-year-from-date).
246+ #
247+ # @return [Integer]
248+ # @see https://www.w3.org/TR/xpath-functions/#func-year-from-date
249+ def year ; Integer . new ( object . year ) ; end
250+
251+ # Months
252+ #
253+ # From the XQuery function [fn:month-from-date](https://www.w3.org/TR/xpath-functions/#func-month-from-date).
254+ #
255+ # @return [Integer]
256+ # @see https://www.w3.org/TR/xpath-functions/#func-month-from-date
257+ def month ; Integer . new ( object . month ) ; end
258+
259+ # Days
260+ #
261+ # From the XQuery function [fn:day-from-date](https://www.w3.org/TR/xpath-functions/#func-day-from-date).
262+ #
263+ # @return [Integer]
264+ # @see https://www.w3.org/TR/xpath-functions/#func-day-from-date
265+ def day ; Integer . new ( object . day ) ; end
115266 end # Date
116267end ; end # RDF::Literal
0 commit comments