class SolveBio::Filter

Attributes

filters[RW]

SolveBio::Filter objects.

Makes it easier to create filters cumulatively using “&“ (and), “|“ (or) and “~“ (not) operations.

Example

require 'solvebio'
f =  SolveBio::Filter.new                       #=> <Filter []>
f &= SolveBio::Filter.new :price => 'Free'      #=> <Filter [[:price, "Free"]]>
f |= SolveBio::Filter.new :style => 'Mexican'   #=> <Filter [{:or=>[[:price, "Free"], [:style, "Mexican"]]}]>

The final result is a filter that can be used in a query which match es “price = 'Free' or style = 'Mexican'”.

By default, each key/value pairs are AND'ed together. However, you can change that to OR by passing in :or as the last argument.

* `<field>='value` matches if the field is term filter (exact term)
* `<field>__in=[<item1>, ...]` matches any of the terms <item1> and so on
* `<field>__range=[<start>, <end>]` matches anything from <start> to <end>

String terms are not analyzed and are always assumed to be exact matches.

Numeric columns can be selected by range using:

* `<field>__gt`: greater than
* `<field>__gte`: greater than or equal to
* `<field>__lt`: less than
* `<field>__lte`: less than or equal to

Field action examples:

dataset.query(:gene__in  => ['BRCA', 'GATA3'],
              :chr       => '3',
              :start__gt => 10000,
              :end__lte  => 20000)

Public Class Methods

deep_copy(obj) click to toggle source
# File lib/solvebio/filter.rb, line 198
def self.deep_copy(obj)
    Marshal.load(Marshal.dump(obj))
end
new(filters={}, conn=:and) click to toggle source

Creates a new Filter, the first argument is expected to be Hash or an Array.

# File lib/solvebio/filter.rb, line 44
def initialize(filters={}, conn=:and)
    if filters.kind_of?(Hash)
        @filters = SolveBio::Filter.
            normalize(filters.keys.sort.map{|key| [key,  filters[key]]})
    elsif filters.kind_of?(Array)
        @filters = filters
    elsif filters.kind_of?(SolveBio::Filter)
        @filters = SolveBio::Filter.deep_copy(filters.filters)
        return self
    else
        raise TypeError, "Invalid filter type #{filters.class}"
    end
    @filters = [{conn => @filters}] if filters.size > 1
    self
end
normalize(ary) click to toggle source

Checks and normalizes filter array tuples

# File lib/solvebio/filter.rb, line 125
def self.normalize(ary)
    ary.map do |tuple|
        unless tuple.kind_of?(Array)
            raise(TypeError,
                  "Invalid filter element #{tuple.class}; want Array")
        end
        unless tuple.size == 2
            raise(TypeError,
                  "Filter element size must be 2; is #{tuple.size}")
        end
        key, value = tuple
        if key.to_s =~ /.+__(.+)$/
            op = $1
            unless %w(gt gte lt lte in range contains prefix regexp).member?(op)
                raise(TypeError,
                      "Invalid field operation #{op} in #{key}")
            end
            case op
            when 'gt', 'gte', 'lt', 'lte'
                begin
                    value = Float(value)
                rescue
                    if /\d{4}-\d{2}-\d{2}/ !~ value
                        raise(TypeError,
                              "Invalid field value #{value} for #{key}; " +
                              "Should be a number or a date in the format 'YYYY-MM-DD'.")
                    end
                end
                tuple = [key, value]
            when 'range'
                if value.kind_of?(Range)
                    value = [value.min, value.max]
                end

                unless value.kind_of?(Array)
                    raise(TypeError,
                          "Invalid field value #{value} for #{key}; " +
                          "Should be an array")
                end
                unless value.size == 2
                    raise(TypeError,
                          "Invalid field value #{value} for #{key}; " +
                          "Array should have exactly two values")
                end
                if value.first > value.last
                    raise(IndexError,
                          "Invalid field value #{value} for #{key}; " +
                          "Start value not greater than end value")
                end
                
                begin
                    Float(value.first)
                    Float(value.last)
                rescue
                    raise(TypeError,
                          "Invalid field values for #{key}; " +
                          "Both should be numbers")
                end

                tuple = [key, value]
            when 'in'
                unless value.kind_of?(Array)
                    raise(TypeError,
                          "Invalid field value #{value} for #{key}; " +
                          "Should be an array")
                end

            end
        end
        tuple
    end
end
process_filters(filters) click to toggle source

Takes a SolveBio::Filter or an Array of filter items and returns an Array that can be passed off (when converted to JSON) to a SolveBio client filter parameter. As such, the output format is highly dependent on the SolveBio API format.

The filter items can be either a SolveBio::Filter, or Hash of the right form, or an Array of the right form.

# File lib/solvebio/filter.rb, line 209
def self.process_filters(filters)
    rv = []
    if filters.kind_of?(SolveBio::Filter)
        if filters.filters
            rv = process_filters(filters.filters)
        end
    else
        filters.each do |f|
            if f.kind_of?(SolveBio::Filter)
                if f.filters
                    rv << process_filters(f.filters)
                    next
                end
            elsif f.kind_of?(Hash)
                key = f.keys[0]
                val = f[key]

                if val.kind_of?(Hash)
                    filter_filters = process_filters(val)
                    if filter_filters.size == 1
                        filter_filters = filter_filters[0]
                    end
                    rv << {key => filter_filters}
                else
                    rv << {key => process_filters(val)}
                end
            else
                rv << f
            end
        end
    end
    return rv
end

Public Instance Methods

&(other) click to toggle source
# File lib/solvebio/filter.rb, line 98
def &(other)
    return self.combine(other, :and)
end
clone() click to toggle source

Deep copy

# File lib/solvebio/filter.rb, line 69
def clone
    SolveBio::Filter.deep_copy(self)
end
combine(other, conn=:and) click to toggle source

OR and AND will create a new Filter, with the filters from both Filter

objects combined with the connector `conn`.

FIXME: should we allow a default conn parameter?

# File lib/solvebio/filter.rb, line 76
def combine(other, conn=:and)
    return other.clone if self.empty?

    if other.empty?
        return self.clone
    elsif self.filters[0].member?(conn)
        f = self.clone
        f.filters[0][conn] += other.filters
    elsif other.filters[0].member?(conn)
        f = other.clone
        f.filters[0][conn] += self.filters
    else
        f = initialize(self.clone.filters + other.filters, conn)
    end

    return f
end
empty?() click to toggle source
# File lib/solvebio/filter.rb, line 64
def empty?
    @filters.empty?
end
inspect() click to toggle source
# File lib/solvebio/filter.rb, line 60
def inspect
    return "<SolveBio::Filter #{@filters.inspect}>"
end
|(other) click to toggle source
# File lib/solvebio/filter.rb, line 94
def |(other)
    return self.combine(other, :or)
end
~() click to toggle source
# File lib/solvebio/filter.rb, line 102
def ~()
    f = self.clone

    # not of null filter is null fiter
    return f if f.empty?

    # length of self_filters should never be more than 1
    filters = f.filters.first
    if filters.kind_of?(Hash) and
        filters.member?(:not)
        # The filters are already a single dictionary
        # containing a 'not'. Swap out the 'not'
        f.filters = [filters[:not]]
    else
        # 'not' blocks can contain only dicts or a single tuple filter
        # so we get the first element from the filter list
        f.filters = [{:not => filters}]
    end

    return f
end