xapit 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. data/CHANGELOG +27 -0
  2. data/Manifest +16 -1
  3. data/README.rdoc +29 -15
  4. data/Rakefile +1 -1
  5. data/features/facets.feature +40 -1
  6. data/features/finding.feature +15 -59
  7. data/features/sorting.feature +29 -0
  8. data/features/step_definitions/xapit_steps.rb +11 -3
  9. data/features/suggestions.feature +17 -0
  10. data/features/support/xapit_helpers.rb +1 -1
  11. data/install.rb +7 -8
  12. data/lib/xapit.rb +34 -2
  13. data/lib/xapit/adapters/abstract_adapter.rb +46 -0
  14. data/lib/xapit/adapters/active_record_adapter.rb +20 -0
  15. data/lib/xapit/adapters/data_mapper_adapter.rb +10 -0
  16. data/lib/xapit/collection.rb +17 -5
  17. data/lib/xapit/config.rb +1 -9
  18. data/lib/xapit/facet.rb +11 -8
  19. data/lib/xapit/index_blueprint.rb +9 -3
  20. data/lib/xapit/indexers/abstract_indexer.rb +13 -2
  21. data/lib/xapit/indexers/classic_indexer.rb +5 -3
  22. data/lib/xapit/indexers/simple_indexer.rb +15 -8
  23. data/lib/xapit/membership.rb +19 -1
  24. data/lib/xapit/query.rb +40 -15
  25. data/lib/xapit/query_parsers/abstract_query_parser.rb +46 -24
  26. data/lib/xapit/rake_tasks.rb +13 -0
  27. data/rails_generators/xapit/USAGE +13 -0
  28. data/rails_generators/xapit/templates/setup_xapit.rb +1 -0
  29. data/rails_generators/xapit/templates/xapit.rake +4 -0
  30. data/rails_generators/xapit/xapit_generator.rb +20 -0
  31. data/spec/spec_helper.rb +2 -2
  32. data/spec/xapit/adapters/active_record_adapter_spec.rb +31 -0
  33. data/spec/xapit/adapters/data_mapper_adapter_spec.rb +10 -0
  34. data/spec/xapit/facet_option_spec.rb +2 -2
  35. data/spec/xapit/index_blueprint_spec.rb +11 -3
  36. data/spec/xapit/indexers/abstract_indexer_spec.rb +37 -0
  37. data/spec/xapit/indexers/classic_indexer_spec.rb +9 -0
  38. data/spec/xapit/indexers/simple_indexer_spec.rb +22 -6
  39. data/spec/xapit/membership_spec.rb +16 -0
  40. data/spec/xapit/query_parsers/abstract_query_parser_spec.rb +21 -3
  41. data/spec/xapit/query_spec.rb +21 -0
  42. data/spec/xapit_member.rb +13 -2
  43. data/tasks/xapit.rake +1 -9
  44. data/tmp/xapiandatabase/postlist.DB +0 -0
  45. data/tmp/xapiandatabase/postlist.baseB +0 -0
  46. data/tmp/xapiandatabase/record.DB +0 -0
  47. data/tmp/xapiandatabase/record.baseB +0 -0
  48. data/tmp/xapiandatabase/spelling.DB +0 -0
  49. data/tmp/xapiandatabase/spelling.baseB +0 -0
  50. data/tmp/xapiandatabase/termlist.DB +0 -0
  51. data/tmp/xapiandatabase/termlist.baseB +0 -0
  52. data/tmp/xapiandatabase/value.DB +0 -0
  53. data/tmp/xapiandatabase/value.baseA +0 -0
  54. data/tmp/xapiandb/spelling.DB +0 -0
  55. data/tmp/xapiandb/spelling.baseB +0 -0
  56. data/xapit.gemspec +4 -4
  57. metadata +23 -3
@@ -0,0 +1,46 @@
1
+ module Xapit
2
+ # Adapters are used to support multiple ORMs (ActiveRecord, Datamapper, Sequel, etc.).
3
+ # It abstracts out all find calls so they can be handled differently for each ORM.
4
+ # To create your own adapter, subclass AbstractAdapter and override the placeholder methods.
5
+ # See ActiveRecordAdapter for an example.
6
+ class AbstractAdapter
7
+ def self.inherited(subclass)
8
+ @subclasses ||= []
9
+ @subclasses << subclass
10
+ end
11
+
12
+ # Returns all adapter classes.
13
+ def self.subclasses
14
+ @subclasses
15
+ end
16
+
17
+ # Sets the @target instance, this is the class the adapter needs to forward
18
+ # its messages to.
19
+ def initialize(target)
20
+ @target = target
21
+ end
22
+
23
+ # Used to determine if the given adapter should be used for the passed in class.
24
+ # Usually one will see if it inherits from another class (ActiveRecord::Base)
25
+ def self.for_class?(member_class)
26
+ raise "To be implemented in subclass"
27
+ end
28
+
29
+ # Fetch a single record by the given id.
30
+ def find_single(id)
31
+ raise "To be implemented in subclass"
32
+ end
33
+
34
+ # Fetch multiple records from the passed in array of ids.
35
+ def find_multiple(ids)
36
+ raise "To be implemented in subclass"
37
+ end
38
+
39
+ # Iiterate through all records using the given parameters.
40
+ # It should yield to the block and pass in each record individually.
41
+ # The args are the same as those passed from the XapitMember#xapit call.
42
+ def find_each(*args, &block)
43
+ raise "To be implemented in subclass"
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,20 @@
1
+ module Xapit
2
+ # This adapter is used for all ActiveRecord models. See AbstractAdapter for details.
3
+ class ActiveRecordAdapter < AbstractAdapter
4
+ def self.for_class?(member_class)
5
+ member_class.ancestors.map(&:to_s).include? "ActiveRecord::Base"
6
+ end
7
+
8
+ def find_single(id)
9
+ @target.find(id)
10
+ end
11
+
12
+ def find_multiple(ids)
13
+ @target.find(ids)
14
+ end
15
+
16
+ def find_each(*args, &block)
17
+ @target.find_each(*args, &block)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,10 @@
1
+ module Xapit
2
+ # This adapter is used for all DataMapper models. See AbstractAdapter for details.
3
+ class DataMapperAdapter < AbstractAdapter
4
+ def self.for_class?(member_class)
5
+ member_class.ancestors.map(&:to_s).include? "DataMapper::Resource"
6
+ end
7
+
8
+ # TODO override the rest of the methods here...
9
+ end
10
+ end
@@ -18,7 +18,7 @@ module Xapit
18
18
  collection = new(member.class, *args)
19
19
  indexer = SimpleIndexer.new(member.class.xapit_index_blueprint)
20
20
  terms = indexer.text_terms(member) + indexer.field_terms(member)
21
- collection.base_query.and_query(Xapian::Query.new(Xapian::Query::OP_OR, terms))
21
+ collection.base_query.and_query(terms, :or)
22
22
  collection.base_query.not_query("Q#{member.class}-#{member.id}")
23
23
  collection
24
24
  end
@@ -103,7 +103,7 @@ module Xapit
103
103
  # Xapit::Facet objects matching this search query. See class for details.
104
104
  def facets
105
105
  all_facets.select do |facet|
106
- facet.options.size > 1
106
+ facet.options.size > 0
107
107
  end
108
108
  end
109
109
 
@@ -118,7 +118,7 @@ module Xapit
118
118
  # If you set :breadcrumb_facets option to true in Config#setup the link will drop leftover facets
119
119
  # instead of removing the current one. This makes it easy to add a breadcrumb style interface.
120
120
  #
121
- # Config.setup(:breadcrumb_facets => true)
121
+ # Xapit.setup(:breadcrumb_facets => true)
122
122
  # <% for option in @articles.applied_facet_options %>
123
123
  # <%= link_to h(option.name), :overwrite_params => { :facets => option } %> &gt;
124
124
  # <% end %>
@@ -153,10 +153,22 @@ module Xapit
153
153
  @query_parser.query.matchset(options)
154
154
  end
155
155
 
156
+ # TODO this could use some refactoring
157
+ # See issue #11 for why this is so complex.
156
158
  def fetch_results(options = {})
157
- matchset(options).matches.map do |match|
159
+ matches = matchset(options).matches
160
+ records_by_class = {}
161
+ matches.each do |match|
158
162
  class_name, id = match.document.data.split('-')
159
- member = class_name.constantize.find(id)
163
+ records_by_class[class_name] ||= []
164
+ records_by_class[class_name] << id
165
+ end
166
+ records_by_class.each do |class_name, ids|
167
+ records_by_class[class_name] = class_name.constantize.xapit_adapter.find_multiple(ids)
168
+ end
169
+ matches.map do |match|
170
+ class_name, id = match.document.data.split('-')
171
+ member = records_by_class[class_name].detect { |m| m.id == id.to_i }
160
172
  member.xapit_relevance = match.percent
161
173
  member
162
174
  end
data/lib/xapit/config.rb CHANGED
@@ -4,15 +4,7 @@ module Xapit
4
4
  class << self
5
5
  attr_reader :options
6
6
 
7
- # Setup configuration options. The following options are supported.
8
- #
9
- # <tt>:database_path</tt>: Where the database is stored.
10
- # <tt>:stemming</tt>: The language to use for stemming, defaults to "english".
11
- # <tt>:spelling</tt>: True or false to enable/disable spelling, defaults to true.
12
- # <tt>:indexer</tt>: Class to handle the indexing, defaults to SimpleIndexer.
13
- # <tt>:query_parser</tt>: Class to handle the parsing, defaults to ClassicQueryParser.
14
- # <tt>:breadcrumb_facets</tt>: Use breadcrumb mode for applied facets. See Collection#applied_facet_options for details.
15
- #
7
+ # See Xapit#setup
16
8
  def setup(options = {})
17
9
  if @options && options[:database_path] != @options[:database_path]
18
10
  @database = nil
data/lib/xapit/facet.rb CHANGED
@@ -23,19 +23,22 @@ module Xapit
23
23
  # Xapit::FacetOption objects for this facet. This only lists the ones which match the current query.
24
24
  def options
25
25
  matching_identifiers.map do |identifier, count|
26
- option = FacetOption.find(identifier)
27
- option.count = count
28
- option.existing_facet_identifiers = @existing_facet_identifiers
29
- option
30
- end.sort_by(&:name)
26
+ if count < @query.count
27
+ option = FacetOption.find(identifier)
28
+ if option.facet.attribute == @blueprint.attribute
29
+ option.count = count
30
+ option.existing_facet_identifiers = @existing_facet_identifiers
31
+ option
32
+ end
33
+ end
34
+ end.compact.sort_by(&:name)
31
35
  end
32
36
 
33
37
  def matching_identifiers
34
38
  result = {}
35
39
  matches.each do |match|
36
- class_name, id = match.document.data.split('-')
37
- record = class_name.constantize.find(id)
38
- @blueprint.identifiers_for(record).each do |identifier|
40
+ identifiers = match.document.terms.map(&:term).grep(/^F/).map { |t| t[1..-1] }
41
+ identifiers.each do |identifier|
39
42
  unless existing_facet_identifiers.include? identifier
40
43
  result[identifier] ||= 0
41
44
  result[identifier] += (match.collapse_count + 1)
@@ -72,19 +72,25 @@ module Xapit
72
72
  end
73
73
 
74
74
  # Indexes all records of this blueprint class. It does this using the ".find_each" method on the member class.
75
- # You will likely want to call Xapit::Config.remove_database before this.
75
+ # You will likely want to call Xapit.remove_database before this.
76
76
  def index_all
77
- @member_class.find_each(*@args) do |member|
77
+ @member_class.xapit_adapter.find_each(*@args) do |member|
78
78
  @indexer.add_member(member)
79
79
  end
80
80
  end
81
81
 
82
- def sortable_position_for(sortable_attribute)
82
+ def position_of_sortable(sortable_attribute)
83
83
  index = sortable_attributes.map(&:to_s).index(sortable_attribute.to_s)
84
84
  raise "Unable to find indexed sortable attribute \"#{sortable_attribute}\" in #{@member_class} sortable attributes: #{sortable_attributes.inspect}" if index.nil?
85
85
  index + facets.size
86
86
  end
87
87
 
88
+ def position_of_field(field_attribute)
89
+ index = field_attributes.map(&:to_s).index(field_attribute.to_s)
90
+ raise "Unable to find indexed field attribute \"#{field_attribute}\" in #{@member_class} field attributes: #{field_attributes.inspect}" if index.nil?
91
+ index + facets.size + sortable_attributes.size
92
+ end
93
+
88
94
  private
89
95
 
90
96
  # Make sure all models are loaded - without reloading any that
@@ -71,12 +71,23 @@ module Xapit
71
71
  end
72
72
 
73
73
  def values(member)
74
- facet_values(member) + sortable_values(member)
74
+ facet_values(member) + sortable_values(member) + field_values(member)
75
75
  end
76
76
 
77
77
  def sortable_values(member)
78
78
  @blueprint.sortable_attributes.map do |sortable|
79
- member.send(sortable).to_s.downcase
79
+ value = member.send(sortable)
80
+ value = value.first if value.kind_of? Array
81
+ Xapit.serialize_value(value)
82
+ end
83
+ end
84
+
85
+ # TODO remove duplication with sortable_values
86
+ def field_values(member)
87
+ @blueprint.field_attributes.map do |sortable|
88
+ value = member.send(sortable)
89
+ value = value.first if value.kind_of? Array
90
+ Xapit.serialize_value(value)
80
91
  end
81
92
  end
82
93
 
@@ -3,11 +3,13 @@ module Xapit
3
3
  def index_text_attributes(member, document)
4
4
  term_generator.document = document
5
5
  @blueprint.text_attributes.each do |name, options|
6
- content = member.send(name).to_s
6
+ content = member.send(name)
7
7
  if options[:proc]
8
- index_terms(options[:proc].call(content).reject(&:blank?).map(&:to_s).map(&:downcase), document)
8
+ index_terms(options[:proc].call(content.to_s).reject(&:blank?).map(&:to_s).map(&:downcase), document)
9
+ elsif content.kind_of? Array
10
+ index_terms(content.reject(&:blank?).map(&:to_s).map(&:downcase), document)
9
11
  else
10
- term_generator.index_text(content)
12
+ term_generator.index_text(content.to_s)
11
13
  end
12
14
  end
13
15
  end
@@ -6,21 +6,28 @@ module Xapit
6
6
  document.add_term(term, options[:weight] || 1)
7
7
  database.add_spelling(term) if Config.spelling?
8
8
  end
9
+ if Config.stemming
10
+ stemmed_terms_for_attribute(member, name, options).each do |term|
11
+ document.add_term(term, options[:weight] || 1)
12
+ end
13
+ end
9
14
  end
10
15
  end
11
16
 
12
- def terms_for_attribute(member, name, options)
13
- terms_for_attribute_without_stemming(member, name, options).map do |term|
14
- [term, "Z#{stemmer.call(term)}"]
15
- end.flatten
17
+ def stemmed_terms_for_attribute(member, name, options)
18
+ terms_for_attribute(member, name, options).map do |term|
19
+ "Z#{stemmer.call(term)}"
20
+ end
16
21
  end
17
22
 
18
- def terms_for_attribute_without_stemming(member, name, options)
19
- content = member.send(name).to_s
23
+ def terms_for_attribute(member, name, options)
24
+ content = member.send(name)
20
25
  if options[:proc]
21
- options[:proc].call(content).reject(&:blank?).map(&:to_s).map(&:downcase)
26
+ options[:proc].call(content.to_s).reject(&:blank?).map(&:to_s).map(&:downcase)
27
+ elsif content.kind_of? Array
28
+ content.reject(&:blank?).map(&:to_s).map(&:downcase)
22
29
  else
23
- content.scan(/\w+/u).map(&:downcase)
30
+ content.to_s.scan(/\w+/u).map(&:downcase)
24
31
  end
25
32
  end
26
33
 
@@ -73,6 +73,12 @@ module Xapit
73
73
  # # search based on indexed fields
74
74
  # @articles = Article.search("phone", :conditions => { :category_id => params[:category_id] })
75
75
  #
76
+ # # search for multiple negative conditions (doesn't match 3, 5, or 8)
77
+ # @articles = Article.search(:not_conditions => { :category_id => [3, 5, 8] })
78
+ #
79
+ # # search for range of conditions by number
80
+ # @articles = Article.search(:conditions => { :released_at => 2.years.ago..Time.now })
81
+ #
76
82
  # # manually sort based on any number of indexed fields, sort defaults to most relevant
77
83
  # @articles = Article.search("phone", :order => [:category_id, :id], :descending => true)
78
84
  #
@@ -90,7 +96,19 @@ module Xapit
90
96
  def xapit_index_blueprint
91
97
  @xapit_index_blueprint
92
98
  end
93
-
99
+
100
+ # The Xapit::AbstractAdapter used to perform database queries on.
101
+ def xapit_adapter
102
+ @xapit_adapter ||= begin
103
+ adapter_class = AbstractAdapter.subclasses.detect { |a| a.for_class?(self) }
104
+ if adapter_class
105
+ adapter_class.new(self)
106
+ else
107
+ raise "Unable to find Xapit adapter for class #{self.name}"
108
+ end
109
+ end
110
+ end
111
+
94
112
  # Finds a Xapit::FacetBlueprint for the given attribute.
95
113
  def xapit_facet_blueprint(attribute)
96
114
  result = xapit_index_blueprint.facets.detect { |f| f.attribute.to_s == attribute.to_s }
data/lib/xapit/query.rb CHANGED
@@ -5,24 +5,21 @@ module Xapit
5
5
  class Query
6
6
  attr_reader :default_options, :xapian_query
7
7
 
8
- def initialize(query)
9
- @xapian_query = build_xapian_query(query)
8
+ def initialize(*args)
9
+ @xapian_query = build_xapian_query(*args)
10
10
  @default_options = { :offset => 0, :sort_descending => false }
11
11
  end
12
12
 
13
- def and_query(query)
14
- @xapian_query = Xapian::Query.new(Xapian::Query::OP_AND, @xapian_query, build_xapian_query(query)) unless query.blank?
15
- self
13
+ def and_query(*args)
14
+ merge_query(:and, *args)
16
15
  end
17
16
 
18
- def or_query(query)
19
- @xapian_query = Xapian::Query.new(Xapian::Query::OP_OR, @xapian_query, build_xapian_query(query)) unless query.blank?
20
- self
17
+ def or_query(*args)
18
+ merge_query(:or, *args)
21
19
  end
22
20
 
23
- def not_query(query)
24
- @xapian_query = Xapian::Query.new(Xapian::Query::OP_AND_NOT, @xapian_query, build_xapian_query(query)) unless query.blank?
25
- self
21
+ def not_query(*args)
22
+ merge_query(:not, *args)
26
23
  end
27
24
 
28
25
  def matchset(options = {})
@@ -51,11 +48,39 @@ module Xapit
51
48
 
52
49
  private
53
50
 
54
- def build_xapian_query(query)
55
- if query.kind_of? Xapian::Query
56
- query
51
+ def merge_query(operator, *args)
52
+ @xapian_query = Xapian::Query.new(xapian_operator(operator), @xapian_query, build_xapian_query(*args)) unless args.first.blank?
53
+ self
54
+ end
55
+
56
+ def build_xapian_query(query, operator = :and)
57
+ extract_queries(query, operator).inject(nil) do |query, extra_query|
58
+ if query
59
+ extra_query = extra_query.xapian_query if extra_query.respond_to? :xapian_query
60
+ Xapian::Query.new(xapian_operator(operator), query, extra_query)
61
+ else
62
+ extra_query = extra_query.xapian_query if extra_query.respond_to? :xapian_query
63
+ extra_query
64
+ end
65
+ end
66
+ end
67
+
68
+ def extract_queries(query, operator)
69
+ queries = [query].flatten
70
+ terms = queries.select { |q| q.kind_of? String }
71
+ if terms.empty?
72
+ queries
57
73
  else
58
- Xapian::Query.new(Xapian::Query::OP_AND, [query].flatten)
74
+ (queries - terms) + [Xapian::Query.new(xapian_operator(operator), terms)]
75
+ end
76
+ end
77
+
78
+ def xapian_operator(operator)
79
+ case operator
80
+ when :and then Xapian::Query::OP_AND
81
+ when :or then Xapian::Query::OP_OR
82
+ when :not then Xapian::Query::OP_AND_NOT
83
+ else raise "Unknown Xapian operator #{operator}"
59
84
  end
60
85
  end
61
86
  end
@@ -10,10 +10,10 @@ module Xapit
10
10
  end
11
11
 
12
12
  def query
13
- if (@search_text.split + condition_terms + facet_terms).empty?
13
+ if (@search_text.split + condition_terms + not_condition_terms + facet_terms).empty?
14
14
  base_query
15
15
  else
16
- @query ||= base_query.and_query(xapian_query_from_text(@search_text)).and_query(condition_terms + facet_terms)
16
+ @query ||= base_query.and_query(xapian_query_from_text(@search_text)).and_query(condition_terms + facet_terms).not_query(not_condition_terms)
17
17
  end
18
18
  end
19
19
 
@@ -34,10 +34,10 @@ module Xapit
34
34
  index = @member_class.xapit_index_blueprint
35
35
  if @options[:order].kind_of? Array
36
36
  @options[:order].map do |attribute|
37
- index.sortable_position_for(attribute)
37
+ index.position_of_sortable(attribute)
38
38
  end
39
39
  else
40
- [index.sortable_position_for(@options[:order])]
40
+ [index.position_of_sortable(@options[:order])]
41
41
  end
42
42
  end
43
43
  end
@@ -47,7 +47,7 @@ module Xapit
47
47
  end
48
48
 
49
49
  def initial_query
50
- query = Query.new(Xapian::Query.new(Xapian::Query::OP_OR, initial_query_strings))
50
+ query = Query.new(initial_query_strings, :or)
51
51
  query.default_options[:offset] = offset
52
52
  query.default_options[:limit] = per_page
53
53
  query.default_options[:sort_by_values] = sort_by_values
@@ -68,18 +68,11 @@ module Xapit
68
68
  end
69
69
 
70
70
  def condition_terms
71
- if @options[:conditions]
72
- @options[:conditions].map do |name, value|
73
- if value.kind_of? Time
74
- value = value.to_i
75
- elsif value.kind_of? Date
76
- value = value.to_time.to_i
77
- end
78
- "X#{name}-#{value.to_s.downcase}"
79
- end
80
- else
81
- []
82
- end
71
+ condition_terms_from_hash(@options[:conditions])
72
+ end
73
+
74
+ def not_condition_terms
75
+ condition_terms_from_hash(@options[:not_conditions])
83
76
  end
84
77
 
85
78
  def facet_terms
@@ -97,19 +90,48 @@ module Xapit
97
90
  end
98
91
 
99
92
  def spelling_suggestion
100
- raise "Spelling has been disabled. Enable spelling in Xapit::Config.setup." unless Config.spelling?
101
- if @search_text.downcase.scan(/\w+/).all? { |term| Config.database.get_spelling_suggestion(term).empty? }
93
+ raise "Spelling has been disabled. Enable spelling in Xapit.setup." unless Config.spelling?
94
+ if [@search_text, *@search_text.scan(/\w+/)].all? { |term| term_suggestion(term).nil? }
102
95
  nil
103
96
  else
97
+ return term_suggestion(@search_text) unless term_suggestion(@search_text).blank?
104
98
  @search_text.downcase.gsub(/\w+/) do |term|
105
- suggestion = Config.database.get_spelling_suggestion(term)
106
- if suggestion.blank?
107
- term
99
+ term_suggestion(term) || term
100
+ end
101
+ end
102
+ end
103
+
104
+ def term_suggestion(term)
105
+ suggestion = Config.database.get_spelling_suggestion(term.downcase)
106
+ suggestion.blank? ? nil : suggestion
107
+ end
108
+
109
+ private
110
+
111
+ def condition_terms_from_hash(conditions)
112
+ if conditions
113
+ conditions.map do |name, value|
114
+ if value.kind_of? Array
115
+ Query.new(value.map { |v| condition_term(name, v) }, :or)
116
+ elsif value.kind_of?(Range) && @member_class
117
+ position = @member_class.xapit_index_blueprint.position_of_field(name)
118
+ Xapian::Query.new(Xapian::Query::OP_VALUE_RANGE, position, Xapit.serialize_value(value.begin), Xapit.serialize_value(value.end))
108
119
  else
109
- suggestion
120
+ condition_term(name, value)
110
121
  end
111
- end
122
+ end.flatten
123
+ else
124
+ []
125
+ end
126
+ end
127
+
128
+ def condition_term(name, value)
129
+ if value.kind_of? Time
130
+ value = value.to_i
131
+ elsif value.kind_of? Date
132
+ value = value.to_time.to_i
112
133
  end
134
+ "X#{name}-#{value.to_s.downcase}"
113
135
  end
114
136
  end
115
137
  end