Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Very very basic where conditions, grouping and aggregation completed.…

… Some specs added but more still needed.
  • Loading branch information...
commit 9f9f69645ef10ac5753a29324a11ff615ecafa12 0 parents
Brad Seefeld authored
5 .gitignore
@@ -0,0 +1,5 @@
+*.gem
+.bundle
+Gemfile.lock
+pkg/*
+.rvmrc
10 Gemfile
@@ -0,0 +1,10 @@
+source "http://rubygems.org"
+
+# Specify your gem's dependencies in rgviz_table.gemspec
+gemspec
+
+gem "rgviz"
+
+group :test, :development do
+ gem "rspec-rails"
+end
25 README.rdoc
@@ -0,0 +1,25 @@
+== Rgviz DataTable
+
+An extension to the {rgviz}{} library to provide a non-activerecord way to connect data to Google Visualization clients.
+
+# Sample code
+rows = []
+
+CSV.each_line(file, "r") do |row|
+ rows.add(row)
+end
+
+query = Rgviz::Parser.parse(params[:tq])
+result_rows = Rgviz::QueryExecutor.execute(rows, query)
+
+# result_rows now has the results of the query.
+
+== TODO
+
+In order of needed:
+
+* Implement ordering statements
+* Implement select statements
+* Implement labels
+* Refactor where parsing so that parens and OR statements are supported
+* Implement pivots
6 Rakefile
@@ -0,0 +1,6 @@
+require "bundler"
+Bundler.setup
+require "rspec/core/rake_task"
+
+Bundler::GemHelper.install_tasks
+RSpec::Core::RakeTask.new
11 lib/rgviz/data_table.rb
@@ -0,0 +1,11 @@
+require "rgviz"
+require "rgviz/data_table/query_executor"
+require "rgviz/data_table/column_value_filter"
+require "rgviz/data_table/column"
+require "rgviz/data_table/sum_column"
+
+module Rgviz
+ module DataTable
+
+ end
+end
32 lib/rgviz/data_table/column.rb
@@ -0,0 +1,32 @@
+module Rgviz
+ module DataTable
+ class Column
+
+ def self.factory(statement)
+ col = nil
+ if m = statement.match(/sum\((.*)\)/i)
+ col = Rgviz::DataTable::SumColumn.new(m[1], statement)
+ end
+
+ unless col
+ col = Rgviz::DataTable::Column.new(statement)
+ end
+ col
+ end
+
+ def initialize(col_name, label = nil)
+ @column = col_name
+ @label = label
+ @label ||= col_name
+ end
+
+ def column
+ @column
+ end
+
+ def label
+ @label
+ end
+ end
+ end
+end
47 lib/rgviz/data_table/column_value_filter.rb
@@ -0,0 +1,47 @@
+require "rgviz/data_table/comparison_filter"
+
+module Rgviz
+ module DataTable
+ class ColumnValueFilter < ComparisonFilter
+
+ def initialize(column, value, operator = Rgviz::DataTable::ComparisonFilter::EQUALS)
+ super(operator)
+
+ @column = column
+ @value = value
+ @is_casted = false
+ end
+
+ def match?(row)
+ val = row[@column]
+
+ cast_type(val)
+
+ super(val, @value)
+ end
+
+ def column
+ @column
+ end
+
+ def value
+ @value
+ end
+
+ ##
+ # Cast the raw type to the complex type if needed.
+ def cast_type(complex)
+ return if @is_casted
+
+ if complex.is_a? Integer and @value.respond_to? :to_i
+ @value = @value.to_i
+ elsif complex.is_a? Float and @value.respond_to? :to_f
+ @value = @value.to_f
+ elsif complex.is_a? Time
+ @value = Time.parse(@value)
+ end
+ @is_casted = true
+ end
+ end
+ end
+end
61 lib/rgviz/data_table/comparison_filter.rb
@@ -0,0 +1,61 @@
+module Rgviz
+ module DataTable
+
+ ##
+ # A base filter class that handles the operator logic of the filter.
+ class ComparisonFilter
+
+ EQUALS = "="
+ NOT_EQUALS = "!="
+ LESS_THAN = "<"
+ LESS_THAN_OR_EQUALS = "<="
+ GREATER_THAN = ">"
+ GREATER_THAN_OR_EQUALS = ">="
+
+ def self.operators
+ [EQUALS, NOT_EQUALS, LESS_THAN, LESS_THAN_OR_EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUALS]
+ end
+
+ ##
+ # Initialize the filter with an operator type.
+ #
+ # @param operator [String] The operator type.
+ def initialize(operator)
+ @operator = operator
+ end
+
+ ##
+ # Determine if the two given values match based on the operator.
+ #
+ # @param left [Object] The left hand side of the comparison
+ # @param right [Object] The right hand side of the comparison
+ # @return [Boolean] True if they are a match.
+ def match?(left, right)
+ return case operator
+ when EQUALS
+ left == right
+ when NOT_EQUALS
+ left != right
+ when LESS_THAN
+ left < right
+ when LESS_THAN_OR_EQUALS
+ left <= right
+ when GREATER_THAN
+ left > right
+ when GREATER_THAN_OR_EQUALS
+ left >= right
+ else
+ false
+ end
+ end
+
+ ##
+ # Fetch the operator for this filter.
+ #
+ # @return [String] The operator type
+ def operator
+ @operator
+ end
+ end
+ end
+end
16 lib/rgviz/data_table/max_column.rb
@@ -0,0 +1,16 @@
+module Rgviz
+ module DataTable
+ class MaxColumn
+
+ def evaluate(rows)
+ max = nil
+ rows.each do |row|
+ if max.nil? or max < row[column]
+ max = row[column]
+ end
+ end
+ max
+ end
+ end
+ end
+end
151 lib/rgviz/data_table/query_executor.rb
@@ -0,0 +1,151 @@
+module Rgviz
+ module DataTable
+ class QueryExecutor
+
+ ##
+ # Execute a query against a collection of rows.
+ #
+ # @param rows [Array] An array of row data (Hashes).
+ # @param query [String|Rgviz::Query] The query to execute.
+ # @return [Array] A new data table that has been filtered by the query.
+ def self.execute(rows, query)
+
+ if rows.nil? or rows.empty?
+ return []
+ end
+
+ if query.is_a? String
+ query = Rgviz::Parser.parse(query)
+ end
+
+ # Convert all hash keys to strings.
+ string_keys = []
+ rows.each do |row|
+ temp = {}
+ row.each_pair do |key, value|
+ temp[key.to_s] = value
+ end
+ string_keys << temp
+ end
+
+ # Get the select part of the statement
+ selects = parse_select(query.select)
+
+ # Filter the data.
+ rows = execute_where(string_keys, query.where)
+
+ # Group the data and perform any aggregation.
+ rows = execute_grouping(rows, query.group_by, selects)
+
+ # Perform ordering
+
+ # Perform selects
+
+ rows
+ end
+
+ ##
+ #
+ def self.execute_grouping(rows, raw_group, selects)
+
+ groups = parse_group(raw_group)
+ return rows if groups.empty?
+
+ rows = group(rows, groups, selects)
+ rows
+ end
+
+ def self.parse_select(select)
+ selects = []
+ select.to_s.split(",").each do |select|
+ selects << Rgviz::DataTable::Column.factory(select)
+ end
+ selects
+ end
+
+ def self.group(rows, groups, selects)
+
+ if groups.empty?
+ row = rows.first
+ selects.each do |select|
+ if select.respond_to? :evaluate
+ row[select.label] = select.evaluate(rows)
+ end
+ end
+ return [row]
+ end
+
+ group = groups.shift
+
+ buckets = {}
+ rows.each do |row|
+ buckets[row[group]] ||= []
+ buckets[row[group]] << row
+ end
+
+ rows = []
+ buckets.each_key do |bucket|
+ rows.concat(group(buckets[bucket], groups, selects))
+ end
+ rows
+ end
+
+ ##
+ #
+ #
+ # @param table []
+ # @param where [Rgviz::Where] The where clause part of the query.
+ def self.execute_where(rows, where)
+ return rows unless where
+
+ filters = parse_where(where)
+
+ filters.each do |filter|
+ filtered_rows = []
+ rows.each do |row|
+ if filter.match?(row)
+ filtered_rows << row
+ end
+ end
+ rows = filtered_rows
+ end
+ rows
+ end
+
+ def self.parse_group(group)
+ return [] unless group
+ groups = group.to_s.split(",")
+ groups.each do |group|
+ group.strip!
+ end
+ groups
+ end
+
+ def self.parse_where(where)
+
+ filters = []
+
+ # TODO: First break into groups by parenthesis.
+
+ # this is very naive...
+ ands = where.to_s.split(/(\sand\s)/i)
+ count = 0
+ ands.each do |raw|
+ if count % 2 == 0
+ index = 0
+ operator = nil
+ Rgviz::DataTable::ComparisonFilter.operators.each do |op|
+ operator = op if raw.include?(op)
+ end
+ if operator
+ parts = raw.split(operator)
+ filters << Rgviz::DataTable::ColumnValueFilter.new(parts[0].strip, parts[1].strip, operator)
+ end
+ end
+ count += 1
+ end
+ filters
+ end
+ end
+ end
+end
19 lib/rgviz/data_table/sum_column.rb
@@ -0,0 +1,19 @@
+module Rgviz
+ module DataTable
+ class SumColumn < Rgviz::DataTable::Column
+
+ def evaluate(rows)
+ sum = 0
+ rows.each do |row|
+ raw = row[column]
+ if raw.respond_to? :to_f
+ sum += raw.to_f
+ elsif raw.respond_to? :to_i
+ sum += raw.to_i
+ end
+ end
+ sum
+ end
+ end
+ end
+end
5 lib/rgviz/data_table/version.rb
@@ -0,0 +1,5 @@
+module Rgviz
+ module DataTable
+ VERSION = "0.0.1"
+ end
+end
20 rgviz_data_table.gemspec
@@ -0,0 +1,20 @@
+# -*- encoding: utf-8 -*-
+$:.push File.expand_path("../lib", __FILE__)
+require "rgviz/data_table/version"
+
+Gem::Specification.new do |s|
+ s.name = "rgviz_data_table"
+ s.version = Rgviz::DataTable::VERSION
+ s.authors = ["Brad Seefeld"]
+ s.email = ["brad@urbaninfluence.com"]
+ s.homepage = ""
+ s.summary = %q{TODO: Write a gem summary}
+ s.description = %q{TODO: Write a gem description}
+
+ s.rubyforge_project = "rgviz_data_table"
+
+ s.files = `git ls-files`.split("\n")
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+ s.require_paths = ["lib"]
+end
97 spec/rgviz/data_table/query_executor_spec.rb
@@ -0,0 +1,97 @@
+require "spec_helper"
+
+describe Rgviz::DataTable::QueryExecutor do
+
+ context "parsing where conditions" do
+ it "parses a simple where condition" do
+ filters = Rgviz::DataTable::QueryExecutor.parse_where("age = 25")
+ filters.length.should == 1
+ validate_filter(filters[0], "age", "25", Rgviz::DataTable::ComparisonFilter::EQUALS)
+ end
+
+ it "ignores bad spacing" do
+ filters = Rgviz::DataTable::QueryExecutor.parse_where(" age = 25 ")
+ validate_filter(filters[0], "age", "25", Rgviz::DataTable::ComparisonFilter::EQUALS)
+ end
+
+ it "parses a where condition with an and statement" do
+ filters = Rgviz::DataTable::QueryExecutor.parse_where("age > 25 and eye = blue")
+ filters.length.should == 2
+ validate_filter(filters[0], "age", "25", Rgviz::DataTable::ComparisonFilter::GREATER_THAN)
+ validate_filter(filters[1], "eye", "blue", Rgviz::DataTable::ComparisonFilter::EQUALS)
+ end
+ end
+
+ it "executes a simple equals query with ints" do
+ rows = [{:age => 25}, {:age => 26}]
+ table = Rgviz::DataTable::QueryExecutor.execute(rows, "select * where age = 25")
+ table.length.should == 1
+ end
+
+ it "executes a simple and query with ints" do
+ rows = [{:age => 25, :friends => 3}, {:age => 25, :friends => 7}]
+ table = Rgviz::DataTable::QueryExecutor.execute(rows, "select * where age = 25 and friends > 5")
+ table.length.should == 1
+ end
+
+ it "executes a exclusive query with ints" do
+ rows = [{:age => 25}, {:age => 26}]
+ table = Rgviz::DataTable::QueryExecutor.execute(rows, "select * where age < 25 and page > 26")
+ table.length.should == 0
+ end
+
+ it "doesnt fail when no rows are given" do
+ rows = Rgviz::DataTable::QueryExecutor.execute(nil, "select * where age < 3")
+ rows.length.should == 0
+ end
+
+ it "filters dates" do
+ now = Time.at(Time.now.to_i) # Round milliseconds off
+ rows = [{:created_at => now}, {:created_at => now - 100}]
+ rows = Rgviz::DataTable::QueryExecutor.execute(rows, "select * where created_at = '#{now}'")
+ rows.length.should == 1
+ end
+
+ it "parses the group by clause" do
+ groups = Rgviz::DataTable::QueryExecutor.parse_group("age, location, date")
+ groups.length.should == 3
+ end
+
+ it "does not fail when group by clause is nil" do
+ groups = Rgviz::DataTable::QueryExecutor.parse_group(nil)
+ groups.length.should == 0
+ end
+
+ it "performs grouping without aggregation" do
+ rows = [{:column => 1}, {:column => 1}, {:column => 2}]
+ rows = Rgviz::DataTable::QueryExecutor.execute(rows, "select * group by column")
+ rows.length.should == 2
+ end
+
+ it "performs multiple column grouping without aggregation" do
+ rows = [{:column1 => 1}, {:column1 => 1, :column2 => 3}, {:column1 => 1}]
+ rows = Rgviz::DataTable::QueryExecutor.execute(rows, "select * group by column1, column2")
+ rows.length.should == 2
+ end
+
+ it "parses a select with sum" do
+ cols = Rgviz::DataTable::QueryExecutor.parse_select("sum(column), column2")
+ cols.length.should == 2
+ cols.first.class.should == Rgviz::DataTable::SumColumn
+ cols.first.label.should == "sum(column)"
+ end
+
+ it "performs column grouping with a sum" do
+ now = Time.now
+ rows = [{:column => 5, :start => now}, {:column => 2, :start => now}, {:column => 4, :start => now}]
+ rows = Rgviz::DataTable::QueryExecutor.execute(rows, "select sum(column) group by start")
+ rows.length.should == 1
+ rows.first["sum(column)"].should == 11
+ end
+
+ def validate_filter(filter, column, value, operator)
+ filter.operator.should == operator
+ filter.column.should == column
+ filter.value.should == value
+ end
+end
1  spec/spec_helper.rb
@@ -0,0 +1 @@
+require "rgviz/data_table"
Please sign in to comment.
Something went wrong with that request. Please try again.