xapit 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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