module RDF; class Literal ## # A date/time literal. # # @see http://www.w3.org/TR/xmlschema-2/#dateTime # @since 0.2.1 class DateTime < Literal DATATYPE = XSD.dateTime GRAMMAR = %r(\A(-?\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?)((?:[\+\-]\d{2}:\d{2})|UTC|GMT|Z)?\Z).freeze FORMAT = '%Y-%m-%dT%H:%M:%SZ'.freeze ## # @param [DateTime] value # @option options [String] :lexical (nil) def initialize(value, options = {}) @datatype = RDF::URI(options[:datatype] || self.class.const_get(:DATATYPE)) @string = options[:lexical] if options.has_key?(:lexical) @string ||= value if value.is_a?(String) @object = case when value.is_a?(::DateTime) then value when value.respond_to?(:to_datetime) then value.to_datetime else ::DateTime.parse(value.to_s) end rescue nil end ## # Converts this literal into its canonical lexical representation. # with date and time normalized to UTC. # # @return [RDF::Literal] `self` # @see http://www.w3.org/TR/xmlschema-2/#dateTime def canonicalize! if self.valid? @string = if has_timezone? @object.new_offset.strftime(FORMAT) else @object.strftime(FORMAT[0..-2]) end end self end ## # Returns the timezone part of arg as a simple literal. Returns the empty string if there is no timezone. # # @return [RDF::Literal] # @see http://www.w3.org/TR/sparql11-query/#func-tz def tz zone = has_timezone? ? object.zone : "" zone = "Z" if zone == "+00:00" RDF::Literal(zone) end ## # Returns the timezone part of arg as an xsd:dayTimeDuration, or `nil` # if lexical form of literal does not include a timezone. # # @return [RDF::Literal] def timezone if tz == 'Z' RDF::Literal("PT0S", :datatype => RDF::XSD.dayTimeDuration) elsif md = tz.to_s.match(/^([+-])?(\d+):(\d+)?$/) plus_minus, hour, min = md[1,3] plus_minus = nil unless plus_minus == "-" hour = hour.to_i min = min.to_i res = "#{plus_minus}PT#{hour}H#{"#{min}M" if min > 0}" RDF::Literal(res, datatype: RDF::XSD.dayTimeDuration) end end ## # Returns `true` if the value adheres to the defined grammar of the # datatype. # # Special case for date and dateTime, for which '0000' is not a valid year # # @return [Boolean] # @since 0.2.1 def valid? super && object && value !~ %r(\A0000) end ## # Does the literal representation include a timezone? Note that this is only possible if initialized using a string, or `:lexical` option. # # @return [Boolean] # @since 1.1.6 def has_timezone? md = self.to_s.match(GRAMMAR) md && !!md[2] end alias_method :has_tz?, :has_timezone? ## # Returns the `timezone` of the literal. If the ## # Returns the value as a string. # # @return [String] def to_s @string || @object.strftime(FORMAT).sub("+00:00", 'Z') end ## # Returns a human-readable value for the literal # # @return [String] # @since 1.1.6 def humanize(lang = :en) d = object.strftime("%r on %A, %d %B %Y") if has_timezone? zone = if self.tz == 'Z' "UTC" else self.tz end d.sub!(" on ", " #{zone} on ") end d end ## # Equal compares as DateTime objects def ==(other) # If lexically invalid, use regular literal testing return super unless self.valid? case other when Literal::DateTime return super unless other.valid? self.object == other.object when Literal::Time, Literal::Date false else super end end end # DateTime end; end # RDF::Literal