module RDF; class Query ## # An RDF basic graph pattern (BGP) query solution sequence. # # @example Filtering solutions using a hash # solutions.filter(author: RDF::URI("http://ar.to/#self")) # solutions.filter(author: "Gregg Kellogg") # solutions.filter(author: [RDF::URI("http://ar.to/#self"), "Gregg Kellogg"]) # solutions.filter(updated: RDF::Literal(Date.today)) # # @example Filtering solutions using a block # solutions.filter { |solution| solution.author.literal? } # solutions.filter { |solution| solution.title.to_s =~ /^SPARQL/ } # solutions.filter { |solution| solution.price < 30.5 } # solutions.filter { |solution| solution.bound?(:date) } # solutions.filter { |solution| solution.age.datatype == RDF::XSD.integer } # solutions.filter { |solution| solution.name.language == :es } # # @example Reordering solutions based on a variable or proc # solutions.order_by(:updated) # solutions.order_by(:updated, :created) # solutions.order_by(:updated, lambda {|a, b| b <=> a}) # # @example Selecting/Projecting particular variables only # solutions.select(:title) # solutions.select(:title, :description) # solutions.project(:title) # # @example Eliminating duplicate solutions # solutions.distinct # # @example Limiting the number of solutions # solutions.offset(20).limit(10) # # @example Counting the number of matching solutions # solutions.count # solutions.count { |solution| solution.price < 30.5 } # # @example Iterating over all found solutions # solutions.each { |solution| puts solution.inspect } # # @since 0.3.0 class Solutions < Array alias_method :each_solution, :each ## # Returns the number of matching query solutions. # # @overload count # @return [Integer] # # @overload count { |solution| ... } # @yield [solution] # @yieldparam [RDF::Query::Solution] solution # @yieldreturn [Boolean] # @return [Integer] # # @return [Integer] def count(&block) super end ## # Returns an array of the distinct variable names used in this solution # sequence. # # @return [Array] def variable_names @variable_names ||= begin variables = self.inject({}) do |result, solution| solution.each_name do |name| result[name] ||= true end result end variables.keys end end ## # Sets variable names used in these solutions. If not set, the default is determined by the variables used in each solution. # # @param [Array] vars # @return [Array] def variable_names=(vars) @variable_names = vars.map(&:to_sym) end ## # @overload variable? # Returns `false`. # # @return [Boolean] # @overload variable?(variables) # Returns `true` if this solution sequence contains bindings for any of # the given `variables`. # # @param [Array] variables # @return [Boolean] # @see RDF::Query::Solution#variable? # @see RDF::Query#execute def variable?(*args) case args.length when 0 then false when 1 self.any? { |solution| solution.variables?(args.first) } else raise ArgumentError("wrong number of arguments (given #{args.length}, expected 0 or 1)") end end alias_method :variables?, :variable? alias_method :have_variables?, :variable? alias_method :has_variables?, :have_variables? ## # Returns hash of bindings from each solution. Each bound variable will have # an array of bound values representing those from each solution, where a given # solution will have just a single value for each bound variable # @return [Hash{Symbol => Array}] def bindings bindings = {} each do |solution| solution.each do |key, value| bindings[key] ||= [] bindings[key] << value end end bindings end ## # Duplicates each solution. # @return [RDF::Query::Solutions] def dup RDF::Query::Solutions.new(self.compact.map(&:dup)) end ## # Merge solutions in `other` into a new solutions instance. Each solution in `other` is merged into those solutions in `self` that are compatible. # # @param [RDF::Query::Solutions] other # @return [RDF::Query::Solutions] def merge(other) other ||= RDF::Query::Solutions() return other if self.empty? return self if other.empty? RDF::Query::Solutions(self.map do |s1| other.map { |s2| s2.merge(s1) if s2.compatible?(s1) } end.flatten.compact) end ## # Filters this solution sequence by the given `criteria`. # # @param [Hash{Symbol => Object}] criteria # @yield [solution] # @yieldparam [RDF::Query::Solution] solution # @yieldreturn [Boolean] # @return [self] def filter(criteria = {}) @variable_names = nil if block_given? self.reject! do |solution| !yield(solution.is_a?(Solution) ? solution : Solution.new(solution)) end else self.reject! do |solution| solution = solution.is_a?(Solution) ? solution : Solution.new(solution) results = criteria.map do |name, value| case value when Array then value.any? {|v| solution[name] == v} when Regexp then solution[name].to_s.match(value) else solution[name] == value end end !results.all? end end self end alias_method :filter!, :filter ## # Difference between solution sets, from SPARQL 1.1. # # The `minus` operation on solutions returns those solutions which either have no compatible solution in `other`, or the solution domains are disjoint. # # @param [RDF::Query::Solutions] other # @return [RDF::Query::Solutions] a new solution set # @see http://www.w3.org/TR/2013/REC-sparql11-query-20130321/#defn_algMinus def minus(other) self.dup.filter! do |soln| !other.any? {|soln2| soln.compatible?(soln2) && !soln.disjoint?(soln2)} end end ## # Reorders this solution sequence by the given `variables`. # # Variables may be symbols or {Query::Variable} instances. # A variable may also be a Procedure/Lambda, compatible with `::Enumerable#sort`. # This takes two arguments (solutions) and returns -1, 0, or 1 equivalently to <=>. # # If called with a block, variables are ignored, and the block is invoked with # pairs of solutions. The block is expected to return -1, 0, or 1 equivalently to <=>. # # @param [Array] variables # @yield [solution] # @yieldparam [RDF::Query::Solution] q # @yieldparam [RDF::Query::Solution] b # @yieldreturn [Integer] -1, 0, or 1 depending on value of comparator # @return [self] def order(*variables) if variables.empty? && !block_given? raise ArgumentError, "wrong number of arguments (0 for 1)" else self.sort! do |a, b| if block_given? yield((a.is_a?(Solution) ? a : Solution.new(a)), (b.is_a?(Solution) ? b : Solution.new(b))) else # Try each variable until a difference is found. variables.inject(nil) do |memo, v| memo || begin comp = v.is_a?(Proc) ? v.call(a, b) : (v = v.to_sym; a[v] <=> b[v]) comp == 0 ? false : comp end end || 0 end end end self end alias_method :order_by, :order ## # Restricts this solution sequence to the given `variables` only. # # @param [Array] variables # @return [self] def project(*variables) if variables.empty? raise ArgumentError, "wrong number of arguments (0 for 1)" else variables.map!(&:to_sym) self.each do |solution| solution.bindings.delete_if { |k, v| !variables.include?(k.to_sym) } end end # Make sure variable_names are ordered by projected variables projected_vars, vars = variables.map(&:to_sym), variable_names vars = variable_names # Maintain projected order, and add any non-projected variables @variable_names = (projected_vars & vars) + (vars - projected_vars) self end alias_method :select, :project ## # Ensures that the solutions in this solution sequence are unique. # # @return [self] def distinct self.uniq! self end alias_method :distinct!, :distinct alias_method :reduced, :distinct alias_method :reduced!, :distinct ## # Limits this solution sequence to bindings starting from the `start` # offset in the overall solution sequence. # # @param [Integer, #to_i] start # zero or a positive or negative integer # @return [self] def offset(start) case start = start.to_i when 0 then nil else self.slice!(0...start) end self end alias_method :offset!, :offset ## # Limits the number of solutions in this solution sequence to a maximum # of `length`. # # @param [Integer, #to_i] length # zero or a positive integer # @return [self] # @raise [ArgumentError] if `length` is negative def limit(length) length = length.to_i raise ArgumentError, "expected zero or a positive integer, got #{length}" if length < 0 case length when 0 then self.clear else self.slice!(length..-1) if length < self.size end self end alias_method :limit!, :limit ## # Equivalence of solution def eql?(other) super && (!other.respond_to?(:variable_names) || variable_names.eql?(other.variable_names)) end ## # Equals of solution def ==(other) super && (!other.respond_to?(:variable_names) || variable_names.eql?(other.variable_names)) end end # Solutions end; end # RDF::Query