lib/conceptql/operators/temporal_operator.rb in conceptql-0.2.0 vs lib/conceptql/operators/temporal_operator.rb in conceptql-0.3.0

- old
+ new

@@ -1,34 +1,100 @@ require_relative 'binary_operator_operator' +require_relative '../date_adjuster' module ConceptQL module Operators # Base class for all temporal operators # # Subclasses must implement the where_clause method which should probably return - # a proc that can be executed as a Sequel "virtual row" e.g. - # Proc.new { l.end_date < r.start_date } + # a Sequel expression to use for filtering. class TemporalOperator < BinaryOperatorOperator reset_categories - category %w(Temporal Relative) + category "Filter by Comparing" + default_query_columns + option :within, type: :string, instructions: 'Enter a numeric value and specify "d", "m", or "y" for "days", "months", or "years". Negative numbers change dates prior to the existing date. Example: -30d = 30 days before the existing date.' + option :at_least, type: :string, instructions: 'Enter a numeric value and specify "d", "m", or "y" for "days", "months", or "years". Negative numbers change dates prior to the existing date. Example: -30d = 30 days before the existing date.' + option :occurrences, type: :integer, desc: "Number of occurrences that must precede the event of interest, e.g. if you'd like the 4th event in a set of events, set occurrences to 3" + + validate_option DateAdjuster::VALID_INPUT, :within, :at_least + validate_option /\A\d+\z/, :occurrences + + def self.within_skip(type) + define_method(:"within_check_#{type}?"){false} + end + def query(db) - db.from(db.from(left_stream(db)) - .join(right_stream(db), l__person_id: :r__person_id) - .where(where_clause) - .select_all(:l)) + ds = db.from(left_stream(db)) + .join(right_stream(db), l__person_id: :r__person_id) + .where(where_clause) + .select_all(:l) + + ds = add_option_conditions(ds) + ds.from_self end + def add_option_conditions(ds) + if within = options[:within] + ds = add_within_condition(ds, within) + end + + if at_least = options[:at_least] + ds = add_within_condition(ds, at_least, :exclude) + end + + if occurrences = options[:occurrences] + ds = add_occurrences_condition(ds, occurrences) + end + + ds + end + + def add_within_condition(ds, within, meth=:where) + within = DateAdjuster.new(within) + after = within.adjust(:r__start_date, true) + before = within.adjust(:r__end_date) + within_col = Sequel.expr(within_column) + ds = ds.send(meth){within_col >= after} if within_check_after? + ds = ds.send(meth){within_col <= before} if within_check_before? + ds.distinct + end + + def add_occurrences_condition(ds, occurrences) + occurrences_col = occurrences_column + ds.distinct.from_self + .select_append{row_number{}.over(:partition => :person_id, :order => occurrences_col).as(:occurrence)} + .from_self + .select(*query_columns(ds)) + .where{occurrence > occurrences.to_i} + end + + def within_column + :l__start_date + end + + def occurrences_column + :start_date + end + + def within_check_after? + true + end + + def within_check_before? + true + end + def inclusive? options[:inclusive] end def left_stream(db) - Sequel.expr(left.evaluate(db)).as(:l) + Sequel.expr(left.evaluate(db).from_self).as(:l) end def right_stream(db) - Sequel.expr(right.evaluate(db)).as(:r) + Sequel.expr(right.evaluate(db).from_self).as(:r) end end end end