Skip to content

Commit

Permalink
Very very basic where conditions, grouping and aggregation completed.…
Browse files Browse the repository at this point in the history
… Some specs added but more still needed.
  • Loading branch information
Brad Seefeld committed Jun 6, 2011
0 parents commit 9f9f696
Show file tree
Hide file tree
Showing 15 changed files with 506 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
@@ -0,0 +1,5 @@
*.gem
.bundle
Gemfile.lock
pkg/*
.rvmrc
10 changes: 10 additions & 0 deletions 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 changes: 25 additions & 0 deletions 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 changes: 6 additions & 0 deletions Rakefile
@@ -0,0 +1,6 @@
require "bundler"
Bundler.setup
require "rspec/core/rake_task"

Bundler::GemHelper.install_tasks
RSpec::Core::RakeTask.new
11 changes: 11 additions & 0 deletions 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 changes: 32 additions & 0 deletions 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 changes: 47 additions & 0 deletions 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 changes: 61 additions & 0 deletions 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 changes: 16 additions & 0 deletions 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 changes: 151 additions & 0 deletions 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

0 comments on commit 9f9f696

Please sign in to comment.