# File lib/jungle_path/query/engine.rb, line 846 def self.primary_keys_match?(row_a, row_b, pk_fld_count) # most common: if pk_fld_count == 1 if row_a.first[1] == row_b.first[1] return true end end # multiple PKs: values_a = row_a.values values_b = row_b.values values_a.each_index do |i| if i == pk_fld_count break end if values_a[i] != values_b[i] return false end end return true end
class JunglePath::Query::Engine
Attributes
apply_limit_offset_to_sql[R]
db[R]
identity[R]
root[R]
tables[R]
Public Class Methods
add_array(symbols, hash)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 828 def self.add_array(symbols, hash) result = {} hash.each do |name, value| if value.class == Hash symbol = symbols.pop if symbol == "<=" result[name] = [] result[name] << add_array(symbols, value) else result[name] = add_array(symbols, value) end else result[name] = value end end result end
add_arrays(query, rows)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 819 def self.add_arrays(query, rows) results = [] rows.each do |hash| symbols = query.symbols.reverse results << add_array(symbols, hash) end results end
calc_paths(query)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 695 def self.calc_paths(query) # For each sort field, find the path to its value in the nested hash. # Returns an array containing an arrays of hash keys. (One array of keys for each sort field.) sort_fields = query.root.sort paths = [] sort_fields.each do |sort_field| keys = [] paths << keys if sort_field.entities sort_field.entities.each do |e| keys << e.to_sym end end keys << sort_field.field_name end paths end
calc_sort_orders(query)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 713 def self.calc_sort_orders(query) # For each sort field, add its ordering (:asc or :desc) into the returned array. sort_fields = query.root.sort sort_orders = [] sort_fields.each do |sort_field| sort_orders << sort_field.sort.to_sym end sort_orders end
combine_results(query, rows)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 868 def self.combine_results(query, rows) results = [] aliases = {} query.aliases.values.each do |ai| aliases[ai.name] = ai end rows.each do |new_row| if results.length == 0 results << new_row else begin previous_row = results[-1] if primary_keys_match?(previous_row, new_row, aliases.values[0].primary_key_columns_count) #merge the row... results[-1 ] = merge_hash(previous_row, new_row, aliases) else # add the row... results << new_row end rescue puts "rescue..." puts previous_row puts "" puts new_row puts "" raise end end end results end
connectors()
click to toggle source
# File lib/jungle_path/query/engine.rb, line 671 def self.connectors Set.new(["and", "or"]) end
dataset_to_array(query, dataset)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 679 def self.dataset_to_array(query, dataset) results = [] dataset.each do |row| results << row end results end
merge_hash(a_hash, b_hash, aliases)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 902 def self.merge_hash(a_hash, b_hash, aliases) #pk_fld_counts) h = {} a = a_hash.to_a # [[name, value], [name, value], ... ] b = b_hash.to_a # [[name, value], [name, value], ... ] a.each_index do |i| a_key = a[i][0] # name a_value = a[i][1] # value b_value = b[i][1] # value if a_value.class == Array pk_fld_count = aliases[a_key].primary_key_columns_count h[a_key] = a_value # a array a_hash = a_value[-1] # do we merge last element? b_hash = b_value[0] # compare to value from new row... if primary_keys_match?(a_hash, b_hash, pk_fld_count) h[a_key][-1] = merge_hash(a_hash, b_hash, aliases) else h[a_key] << b_hash # add value from new row to array end elsif a_value.class == Hash pk_fld_count = aliases[a_key].primary_key_columns_count if primary_keys_match?(a_value, b_value, pk_fld_count) h[a_key] = merge_hash(a_value, b_value, aliases) else raise "unexpected multiple values found!!! Expected\na_value: '#{a_value}'\npk to match\nb_value: '#{b_value}'\npk." end else # regular field so a_value and b_value are the same. h[a_key] = a_value end end h end
new(node_tree, identity=nil, apply_limit_offset_to_sql=true, db=nil)
click to toggle source
def initialize(tables_hash, identity=nil, apply_limit_offset_to_sql
=true)
# File lib/jungle_path/query/engine.rb, line 13 def initialize(node_tree, identity=nil, apply_limit_offset_to_sql=true, db=nil) @tables = node_tree.tables_hash @identity = identity @apply_limit_offset_to_sql = apply_limit_offset_to_sql @db = db #@root = Gen.gen_node_tree(tables_hash) @root = node_tree end
operator_map()
click to toggle source
# File lib/jungle_path/query/engine.rb, line 667 def self.operator_map {"==" => "=", "!=" => "!=", ">" => ">", "<" => "<", ">=" => ">=", "<=" => "<=", "~" => "ilike", "=~" => "regex", "~~" => "full text search", ">>" => "in", "<<" => "not in", "is" => "is", "is!" => "is not"} end
operators()
click to toggle source
# File lib/jungle_path/query/engine.rb, line 663 def self.operators Set.new(["==", "!=", ">", "<", ">=", "<=", "~", "=~", "~~", ">>", "<<", "is", "is!"]) end
primary_keys_match?(row_a, row_b, pk_fld_count)
click to toggle source
run(query, db, level=7)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 22 def self.run(query, db, level=7) dataset = run_query(query, db) rows = dataset_to_array(query, dataset) if level > 0 rows = sort_by_ids(query, rows) if level > 1 rows = transform_results_to_hash_array(query, rows) if level > 2 rows = add_arrays(query, rows) if level > 3 rows = combine_results(query, rows) if level > 4 rows = sort_by_original_sort(query, rows) if level > 5 rows = wrap_outer_objects(query, rows) if level > 6 if rows if query.apply_limit_offset_to_sql return rows else puts "query.root.limit: #{query.root.limit}." puts "query.root.offset: #{query.root.offset}." limit = 0 offset = 0 if query.root.limit and query.root.limit > 0 limit = query.root.limit end if query.root.offset and query.root.offset > 0 offset = query.root.offset end puts "rows.length: #{rows.length}." return rows if limit == 0 and offset == 0 if offset > 0 and limit == 0 rows = rows[offset, -1] rows = [] unless rows return rows end rows = rows[offset, limit] rows = [] unless rows return rows end else return dataset end end
run_query(query, db)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 61 def self.run_query(query, db) ds = db[query.sql, *query.values] end
sort_by_ids(query, results)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 687 def self.sort_by_ids(query, results) keys = query.sort_ids if results.length > 0 results = results.sort_by {|h| (h.reject {|x| !keys.include?(x)}).values } end results end
sort_by_original_sort(query, rows)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 727 def self.sort_by_original_sort(query, rows) if query.root.sort and query.root.sort.length > 0 paths = calc_paths(query) sort_orders = calc_sort_orders(query) sorter = JunglePath::Query::NestedHashSorter.new(paths, sort_orders) begin #strange: when I added this debugging code, modes_of_action.uql sorting on code started working... #was getting: ArgumentError - comparison of Hash with Hash failed. #very strange! count = 0 rows = rows.sort do |a, b| count += 1 val = sorter.sort(a, b) val end rescue puts "count: #{count}." raise end end rows end
sorts()
click to toggle source
# File lib/jungle_path/query/engine.rb, line 675 def self.sorts Set.new(["asc", "desc"]) end
transform_results_to_hash_array(query, rows)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 750 def self.transform_results_to_hash_array(query, rows) # transform results into nested hashes (for converting to json later...) results = [] rows.each do |row| #binding.pry a_stack = [] h_stack = [] current_alias = nil current_hash = nil row.each do |k, v| #each item in this row... new_alias, field_name = k.to_s.split(".").map{|a| a.to_sym} parent_alias = query.aliases[new_alias].parent_alias #parent_alias = parent_alias.to_sym if parent_alias #load stack: if current_alias a_stack.push(current_alias) h_stack.push(current_hash) end #unload stack: index = a_stack.index(new_alias) if index # We've seen this new alias before. Get it (and the related hash) out of the stacks: begin current_alias = a_stack.pop current_hash = h_stack.pop end until current_alias == new_alias current_hash[field_name] = v elsif current_alias if a_stack.include?(parent_alias) #get parent hash begin temp_parent_alias = a_stack.pop temp_parent_hash = h_stack.pop end until temp_parent_alias == parent_alias # put new hash into parent hash: new_hash = {} temp_parent_hash[query.aliases[new_alias.to_sym].name ] = new_hash # (convert alias to entity name) a_stack.push temp_parent_alias h_stack.push temp_parent_hash # we now have new currents: current_alias = new_alias current_hash = new_hash current_hash[field_name] = v else raise "parent_alias: #{parent_alias} was not found in a_stack: #{a_stack}. (h_stack: #{h_stack}).\nresults:\n#{results}." end else #completely new: current_alias = new_alias current_hash = {} current_hash[field_name] = v end end # get the earliest parent (which contains all of the rest) and add to results: while h_stack.length > 0 current_hash = h_stack.pop end results << current_hash #break # <= for debugging only end results end
wrap_outer_objects(query, rows)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 723 def self.wrap_outer_objects(query, rows) rows.map {|row| {query.root.name => row}} end
Public Instance Methods
find_table_entity(name, parent_entity, level=0, entities=nil)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 492 def find_table_entity(name, parent_entity, level=0, entities=nil) #binding.pry entities = [] unless entities entity = nil parent_entity.fields.each do |field| if field.is_entity? and field.name == name entities << name entity = field break elsif field.is_entity? entities.push field.name entity, entities = find_table_entity(name, field, level + 1, entities) if entity break end entities.pop end end return entity, entities end
get_entity_child_node(token, entity)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 470 def get_entity_child_node(token, entity) #puts "entity: #{entity.name}." #puts "token: #{token}." #puts "entity.node: #{entity.node}." field_name = JunglePath::Query::Entity.get_name_from_token(token) #puts "field_name: #{field_name}." if entity.node.nodes.length > 0 node = entity.node.nodes[field_name] else #puts "entity.node.child_table_name: #{entity.node.child_table_name}." node = @root.nodes[entity.node.child_table_name].nodes[field_name] end #new: if node and node.class == ::Array #see gen_node_tree.rb line 60+. # node is actually array of nodes, so pick correct one... token s/have column indicator, otherwise just grab first one. node = JunglePath::Query::Entity.select_node_from_array_by_token(node, token) end node end
get_entity_node(token, parent_entity)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 448 def get_entity_node(token, parent_entity) name = JunglePath::Query::Entity.get_name_from_token(token) if parent_entity node = parent_entity.node.nodes[name] unless node if parent_entity.node.child_table_name node = @root.nodes[parent_entity.node.child_table_name].nodes[name] else node = @root.nodes[parent_entity.name].nodes[name] end end end node = @root.nodes[name] unless node #new: if node and node.class == ::Array node = JunglePath::Query::Entity.select_node_from_array_by_token(node, token) end node end
get_query_from_string(query_string)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 65 def get_query_from_string(query_string) tokens = tokenize(query_string).reverse entity_root, values = parse_tokens(tokens) #sql, aliases, symbols, sort_ids, primary_key_field_count = Engine.generate_sql @tables, (entity_root) sql, aliases, symbols, sort_ids, primary_key_field_count = JunglePath::Query::SQLString.generate( self, (entity_root) ) JunglePath::Query::Query.new(entity_root, sql, values, aliases, symbols, sort_ids, primary_key_field_count, @apply_limit_offset_to_sql) end
next_token(tokens)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 82 def next_token(tokens) token = tokens.pop token end
parse_tokens(tokens)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 73 def parse_tokens(tokens) aliases = ('a'..'z').to_a.reverse doubles = aliases.map{|a| a + a} #also add in "aa", "bb", ... "zz" aliases = doubles.push(*aliases) values = [] entity_root = process_entity(nil, nil, tokens, aliases, values) return entity_root, values end
process_entity(parent_entity, token, tokens, aliases, values, symbol=nil, root_entity=nil, in_fields=false)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 87 def process_entity(parent_entity, token, tokens, aliases, values, symbol=nil, root_entity=nil, in_fields=false) #puts "process_entity - parent: #{parent_entity != nil}; token: #{token}." expect_entity = false expect_fields = false expect_filter = false expect_sort = false expect_limit = false expect_offset = false entity = nil token = next_token(tokens) unless token while token if !entity expect_entity = true if token =~ /^[a-zA-Z_]/ # starts with alpha or underscore #binding.pry node = get_entity_node(token, parent_entity) if node entity = JunglePath::Query::Entity.new(token, aliases.pop, node, parent_entity, symbol) root_entity = entity unless root_entity expect_entity = false expect_fields = true else raise "Entity #{token} was not found." end else raise "Expected token to be an entity name. Token: #{token}" end elsif expect_offset and token == "[" #token.start_with? "[" expect_offset = false #entity.offset = JunglePath::Query::Limit.parse(token) #same format as Limit. entity.offset = process_limit(tokens) # same as limit elsif expect_limit and token == "[" #token.start_with? "[" expect_limit = false #entity.limit = JunglePath::Query::Limit.parse(token) entity.limit = process_limit(tokens) expect_offset = true elsif expect_sort and token == "(" if in_fields raise "Sorts need to go in outer query -- they cannot be nested. Qualify fields using entity names. No qualification is needed for the outer entity." end expect_sort = false entity.sort = process_sort(entity, tokens, aliases, root_entity) expect_limit = true elsif expect_filter and token == "(" expect_filter = false entity.filter = process_filter(entity, tokens, aliases, values, root_entity) if in_fields and tokens.length > 0 and (tokens.last == ")" or tokens.last == ",") #puts "break" break end expect_sort = true elsif expect_fields and token == "[" # got parameters which should be passed to the entity in sql as a table function. entity.parameters = process_parameters(tokens) elsif expect_fields and token == "(" expect_fields = false entity.fields = process_fields(entity, tokens, aliases, values, root_entity) #puts "back in entity" if in_fields and tokens.length > 0 and (tokens.last == ")" or tokens.last == ",") #puts "break" break end expect_filter = true else raise "Unexpected token: #{token}." end token = next_token(tokens) end entity end
process_fields(entity, tokens, aliases, values, root_entity)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 203 def process_fields(entity, tokens, aliases, values, root_entity) #puts "process_fields - entity.name: #{entity.name}." fields = [] table = nil token = next_token(tokens) while token #puts "token: #{token}." if token == ")" break elsif token == "," # do nothing elsif token =~ /^[a-zA-Z_]/ # starts with alpha or underscore node = get_entity_child_node(token, entity) #Force PK fields onto field list: if fields.length == 0 table_name = entity.node.child_table_name table_name = entity.name unless table_name #puts "#{entity.name}." #puts "#{entity.node.child_table_name}." table = tables[table_name] table.primary_key_columns.values.each do |pk_column| fields << JunglePath::Query::FieldPrimaryKey.new(pk_column.name, node, entity) end end if node and node.child_table_name # actually a table (entity). field = process_entity(entity, token, tokens, aliases, values, node.symbol, root_entity, true) field.field_node = node fields << field elsif node #puts "token, node, entity: #{token}, #{node.name}, #{entity.name}." field_symbol = token.downcase.to_sym is_secure = table.secure_columns.include? field_symbol fields << JunglePath::Query::Field.new(token, node, entity, is_secure) unless fields.map{|n| n.name}.include?(field_symbol) else raise "Token #{token} is not a valid table nor column name for entity: #{entity.name}.\nchild nodes: #{entity.node.nodes}." end else raise "Token #{token} is not a valid table nor column name." end token = next_token(tokens) end fields end
process_filter(entity, tokens, aliases, values, root_entity)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 250 def process_filter(entity, tokens, aliases, values, root_entity) #puts "process_filter..." filter = [] nest_count = 0 expect_field = true expect_value = false expect_operator = false expect_list = false expect_connector = false expect_comma = false token = next_token(tokens) #puts "token: #{token}" while token if expect_operator and JunglePath::Query::Engine.operators.include? token expect_operator = false filter << JunglePath::Query::Operator.parse(token) if token == ">>" or token == "<<" expect_list = true else expect_value = true end elsif expect_operator raise "Expected token #{token} to be one of the following operators: #{Engine.operators.inspect}." elsif expect_field and token == "(" nest_count += 1 filter << token elsif expect_field and token == ")" and nest_count > 0 nest_count -= 1 filter << token elsif expect_field and token == ")" break elsif expect_value and token == 'null' # values << token #skip this token will use the literal null in filter. filter << "null" expect_value = false expect_field = true expect_connector = true elsif expect_value and token =~ /^[0-9]/ # starts with number if token.include? "." values << JunglePath::Query::FloatValue.parse(token) filter << "?" else values << JunglePath::Query::IntValue.parse(token) filter << "?" end expect_value = false expect_field = true expect_connector = true elsif expect_value and token =~ /^["']/ # starts with quote values << JunglePath::Query::StringValue.parse(token) filter << "?" expect_value = false expect_field = true expect_connector = true elsif expect_value and token.downcase == "true" values << true filter << "?" expect_value = false expect_field = true expect_connector = true elsif expect_value and token.downcase == "false" values << false filter << "?" expect_value = false expect_field = true expect_connector = true elsif expect_list and token == "(" filter << "(" filter << process_value_list(entity, tokens, aliases, root_entity, values) expect_list = false expect_field = true elsif expect_field and expect_connector and JunglePath::Query::Engine.connectors.include? token.downcase filter << token.downcase expect_connector = false elsif expect_field and Engine.connectors.include? token.downcase raise "Did not expect connector token #{token}." elsif expect_field and token =~ /^[a-zA-Z_]/ # starts with alpha or underscore node = get_entity_child_node(token, entity) if node and !node.child_table_name #filter << Field.new(name, :column, node) filter << "#{entity.alias_}.#{node.name}" expect_field = false expect_connector = false expect_operator = true else raise "Token #{token} is not a valid column name." end else raise "Token #{token} is not a valid column name. filter: #{filter.inspect}; tokens: #{tokens.inspect}." end token = next_token(tokens) end filter end
process_limit(tokens)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 157 def process_limit(tokens) limit = nil limit_contents = [] token = next_token(tokens) while token if token == "]" limit = JunglePath::Query::Limit.parse(limit_contents.join(' ')) break else limit_contents << token end token = next_token(tokens) end limit end
process_parameters(tokens)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 173 def process_parameters(tokens) parameters = [] token = next_token(tokens) while token if token == "]" break elsif token == ',' # ignore a comma elsif token == 'null' parameters << nil elsif token =~ /^[0-9]/ # starts with number if token.include? "." parameters << JunglePath::Query::FloatValue.parse(token) else parameters << JunglePath::Query::IntValue.parse(token) end elsif token =~ /^["']/ # starts with quote parameters << JunglePath::Query::StringValue.parse(token) elsif token.downcase == "true" parameters << true elsif token.downcase == "false" parameters << false else raise "unexpected token: #{token}" end token = next_token(tokens) end parameters end
process_sort(entity, tokens, aliases, root_entity)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 390 def process_sort(entity, tokens, aliases, root_entity) sort = [] sort_field = nil expect_sort = false token = next_token(tokens) while token if token == ")" break elsif token == "," #do nothing #sort << ", " elsif expect_sort and JunglePath::Query::Engine.sorts.include? token.downcase expect_sort = false sort[-1].sort = token.downcase elsif token =~ /^[a-zA-Z_]/ # starts with alpha or underscore parts = token.downcase.split('.') if parts.length > 1 table = parts[0].to_sym field = parts[1].to_sym else table = nil field = parts[0].to_sym end table_entity = nil entities = nil if table table_entity, entities = find_table_entity(table, root_entity) raise "Token #{token} leading part #{table} is not a valid table from your query." unless table_entity end if field table_entity = entity unless table_entity node = table_entity.node.nodes[field] #puts "table_entity.name: #{table_entity.name}." #puts "table_entity.node.name: #{table_entity.node.name}." #puts "table_entity.node.nodes: #{table_entity.node.nodes}." #puts "pp node:" #pp node #puts "zzz" if node and node.child_table_name raise "Field #{field} is not a valid field for table #{table} in your query," elsif node == nil and !@root.nodes[table_entity.name].nodes[field] raise "Field #{field} is not a valid field for table #{table_entity.name} in your query," else #sort_field = SortField.new(table, field, table_entity.node, node) #sort << sort_field #sort << "#{table_entity.alias_}.#{field}" sort << JunglePath::Query::SortField.new("#{table_entity.alias_}.#{field}", field, entities) expect_sort = true end end else raise "Token #{token} is not a valid table nor column name (or combination)." end token = next_token(tokens) end sort end
process_value_list(entity, tokens, aliases, root_entity, values)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 345 def process_value_list(entity, tokens, aliases, root_entity, values) list = [] expect_value = true expect_comma = false token = next_token(tokens) while token #puts "token: #{token}." #puts "list: #{list.inspect}." if list.empty? and token == ")" list << ")" break elsif expect_comma and token == ")" list << ")" break elsif expect_comma and token == "," list << ", " expect_comma = false expect_value = true elsif expect_comma raise "Missing comma from 'in (list)'. list: #{list.inspect}; values: #{values.inspect}; tokens: #{tokens.inspect}." elsif expect_value and token == ")" raise "Found unexpected closing parenthisis: #{token} in value list following a comma: (,)." elsif expect_value and token =~ /^[0-9]/ # starts with number if token.include? "." values << JunglePath::Query::FloatValue.filter(token) list << "?" else values << JunglePath::Query::IntValue.parse(token) list << "?" end expect_value = false expect_comma = true elsif expect_value and token =~ /^["']/ # starts with quote values = JunglePath::Query::StringValue.parse(token) list << "?" expect_value = false expect_comma = true else raise "Found unexpected token: #{token} in value list." end token = next_token(tokens) end list end
tokenize(query)
click to toggle source
# File lib/jungle_path/query/engine.rb, line 513 def tokenize(query) tokens = [] in_single_line_comment = nil in_comment = 0 in_string = nil in_escape = false parens = 0 in_bracket = false token = "" i = 0 prev_char = nil next_char = nil query.each_char do |c| i += 1 next_char = query[i] #puts "i: #{i}, c: #{c}, tokens: #{tokens}" if in_escape and c == "\\" in_escape = false token << "\\" elsif in_escape and c == "t" in_escape = false token << "\t" elsif in_escape and c == "n" in_escape = false token << "\n" elsif in_escape and c == "r" in_escape = false token << "\r" elsif in_escape and c == "\'" in_escape = false token << "\'" elsif in_escape and c == "\"" in_escape = false token << "\"" elsif in_escape raise "Invalid character #{c} was escaped. You can only escape as follows: \\\\ \\t \\n \\r \\\' \\\" got it? :)" elsif in_string and c.strip.empty? # is whitespace? token << c elsif in_string and in_string == c token = token << c tokens << token token = "" in_string = nil elsif in_string and c == "\\" in_escape = true elsif in_string token << c elsif in_comment > 0 and c == "*" #do nothing elsif in_comment > 0 and prev_char == "*" and c == "/" in_comment -= 1 elsif in_single_line_comment and (c == "\n" or c == "\r") in_single_line_comment = false elsif in_single_line_comment #do nothing elsif c == "/" and next_char == "*" in_comment += 1 elsif in_comment > 0 #do nothing elsif c == "-" and next_char == "-" if token.length > 0 tokens << token token = "" end in_single_line_comment = true #elsif in_bracket and c == "]" # in_bracket = false # token << c # tokens << token # token = "" #elsif in_bracket # token << c elsif c == "\"" if token.length > 0 tokens << token token = "" end in_string = c token << c elsif c == "'" if token.length > 0 tokens << token token = "" end in_string = c token << c elsif c.strip.empty? # is whitespace? if token.length > 0 tokens << token token = "" end elsif c == "," if token.length > 0 tokens << token token = "" end tokens << "," elsif c == "(" parens += 1 if token.length > 0 tokens << token token = "" end tokens << "(" elsif c == ")" parens -= 1 if parens < 0 raise "Extra ')' character found at #{i}." end if token.length > 0 tokens << token token = "" end tokens << ")" elsif c == "[" in_bracket = true if token.length > 0 tokens << token token = "" end #token << c tokens << "[" elsif in_bracket and c == "]" in_bracket = false if token.length > 0 tokens << token token = "" end tokens << "]" else token << c end prev_char = c end if in_bracket raise "No closing ']' character was found." end if parens > 0 raise "Missing #{parens} ')' characters." end if in_string raise "Unclosed string: missing #{in_string} character." end if token.length > 0 tokens << token token = "" end tokens end