rambling-trie 0.9.3 → 1.0.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -0
  3. data/LICENSE +1 -1
  4. data/README.md +133 -26
  5. data/Rakefile +1 -2
  6. data/lib/rambling/trie.rb +53 -9
  7. data/lib/rambling/trie/comparable.rb +16 -0
  8. data/lib/rambling/trie/compressable.rb +14 -0
  9. data/lib/rambling/trie/compressed_node.rb +38 -14
  10. data/lib/rambling/trie/compressor.rb +14 -10
  11. data/lib/rambling/trie/configuration.rb +11 -0
  12. data/lib/rambling/trie/configuration/properties.rb +66 -0
  13. data/lib/rambling/trie/configuration/provider_collection.rb +101 -0
  14. data/lib/rambling/trie/container.rb +57 -17
  15. data/lib/rambling/trie/enumerable.rb +1 -1
  16. data/lib/rambling/trie/forwardable.rb +9 -4
  17. data/lib/rambling/trie/inspectable.rb +37 -0
  18. data/lib/rambling/trie/invalid_operation.rb +3 -2
  19. data/lib/rambling/trie/missing_node.rb +2 -1
  20. data/lib/rambling/trie/node.rb +40 -30
  21. data/lib/rambling/trie/raw_node.rb +29 -13
  22. data/lib/rambling/trie/readers.rb +11 -0
  23. data/lib/rambling/trie/readers/plain_text.rb +26 -0
  24. data/lib/rambling/trie/serializers.rb +11 -0
  25. data/lib/rambling/trie/serializers/file.rb +25 -0
  26. data/lib/rambling/trie/serializers/marshal.rb +38 -0
  27. data/lib/rambling/trie/serializers/yaml.rb +39 -0
  28. data/lib/rambling/trie/serializers/zip.rb +67 -0
  29. data/lib/rambling/trie/stringifyable.rb +20 -0
  30. data/lib/rambling/trie/version.rb +1 -1
  31. data/rambling-trie.gemspec +2 -2
  32. data/spec/integration/rambling/trie_spec.rb +45 -49
  33. data/spec/lib/rambling/trie/comparable_spec.rb +104 -0
  34. data/spec/lib/rambling/trie/compressed_node_spec.rb +44 -0
  35. data/spec/lib/rambling/trie/configuration/properties_spec.rb +49 -0
  36. data/spec/lib/rambling/trie/configuration/provider_collection_spec.rb +165 -0
  37. data/spec/lib/rambling/trie/container_spec.rb +127 -38
  38. data/spec/lib/rambling/trie/{inspector_spec.rb → inspectable_spec.rb} +7 -5
  39. data/spec/lib/rambling/trie/raw_node_spec.rb +22 -41
  40. data/spec/lib/rambling/trie/readers/plain_text_spec.rb +14 -0
  41. data/spec/lib/rambling/trie/serializers/file_spec.rb +11 -0
  42. data/spec/lib/rambling/trie/serializers/marshal_spec.rb +14 -0
  43. data/spec/lib/rambling/trie/serializers/yaml_spec.rb +14 -0
  44. data/spec/lib/rambling/trie/serializers/zip_spec.rb +30 -0
  45. data/spec/lib/rambling/trie/stringifyable_spec.rb +82 -0
  46. data/spec/lib/rambling/trie_spec.rb +120 -7
  47. data/spec/spec_helper.rb +7 -1
  48. data/spec/support/config.rb +5 -0
  49. data/spec/support/shared_examples/a_compressable_trie.rb +26 -0
  50. data/spec/support/shared_examples/a_serializable_trie.rb +26 -0
  51. data/spec/support/shared_examples/a_serializer.rb +29 -0
  52. data/spec/support/shared_examples/a_trie_data_structure.rb +29 -0
  53. data/spec/tmp/.gitkeep +0 -0
  54. metadata +51 -24
  55. data/lib/rambling/trie/compression.rb +0 -13
  56. data/lib/rambling/trie/inspector.rb +0 -11
  57. data/lib/rambling/trie/plain_text_reader.rb +0 -23
  58. data/lib/rambling/trie/tasks/gem.rb +0 -17
  59. data/lib/rambling/trie/tasks/helpers/path.rb +0 -17
  60. data/lib/rambling/trie/tasks/helpers/performance_report.rb +0 -17
  61. data/lib/rambling/trie/tasks/helpers/time.rb +0 -7
  62. data/lib/rambling/trie/tasks/performance.rb +0 -15
  63. data/lib/rambling/trie/tasks/performance/all.rb +0 -17
  64. data/lib/rambling/trie/tasks/performance/benchmark.rb +0 -201
  65. data/lib/rambling/trie/tasks/performance/directory.rb +0 -11
  66. data/lib/rambling/trie/tasks/performance/flamegraph.rb +0 -119
  67. data/lib/rambling/trie/tasks/performance/profile/call_tree.rb +0 -147
  68. data/lib/rambling/trie/tasks/performance/profile/memory.rb +0 -143
  69. data/spec/lib/rambling/trie/plain_text_reader_spec.rb +0 -18
@@ -1,10 +1,10 @@
1
1
  module Rambling
2
2
  module Trie
3
- # Responsible for the compression process of a Trie data structure.
3
+ # Responsible for the compression process of a trie data structure.
4
4
  class Compressor
5
- # Compresses a node from a Trie data structure.
6
- # @param [RawNode] node the node to compress
7
- # @return [CompressedNode] node the compressed version of the node
5
+ # Compresses a {Node Node} from a trie data structure.
6
+ # @param [RawNode] node the node to compress.
7
+ # @return [CompressedNode] node the compressed version of the node.
8
8
  def compress node
9
9
  if node.compressable?
10
10
  merge_with_child_and_compress node
@@ -18,18 +18,15 @@ module Rambling
18
18
  def merge_with_child_and_compress node
19
19
  child = node.children.first
20
20
 
21
- new_node = Rambling::Trie::CompressedNode.new node.parent
22
- new_node.letter = node.letter.to_s << child.letter.to_s
23
- new_node.terminal! if child.terminal?
21
+ letter = node.letter.to_s << child.letter.to_s
22
+ new_node = new_compressed_node node, letter, child.terminal?
24
23
  new_node.children_tree = child.children_tree
25
24
 
26
25
  compress new_node
27
26
  end
28
27
 
29
28
  def copy_node_and_compress_children node
30
- new_node = Rambling::Trie::CompressedNode.new node.parent
31
- new_node.letter = node.letter
32
- new_node.terminal! if node.terminal?
29
+ new_node = new_compressed_node node, node.letter, node.terminal?
33
30
 
34
31
  node.children.each do |child|
35
32
  compressed_child = compress child
@@ -40,6 +37,13 @@ module Rambling
40
37
 
41
38
  new_node
42
39
  end
40
+
41
+ def new_compressed_node node, letter, terminal
42
+ new_node = Rambling::Trie::CompressedNode.new node.parent
43
+ new_node.letter = letter
44
+ new_node.terminal! if terminal
45
+ new_node
46
+ end
43
47
  end
44
48
  end
45
49
  end
@@ -0,0 +1,11 @@
1
+ %w{properties provider_collection}.each do |file|
2
+ require File.join('rambling', 'trie', 'configuration', file)
3
+ end
4
+
5
+ module Rambling
6
+ module Trie
7
+ # Namespace for configuration classes.
8
+ module Configuration
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,66 @@
1
+ module Rambling
2
+ module Trie
3
+ module Configuration
4
+ # Provides configurable properties for Rambling::Trie.
5
+ class Properties
6
+ # The configured {Readers Readers}.
7
+ # @return [ProviderCollection] the mapping of configured {Readers
8
+ # Readers}.
9
+ attr_reader :readers
10
+
11
+ # The configured {Serializers Serializers}.
12
+ # @return [ProviderCollection] the mapping of configured {Serializers
13
+ # Serializers}.
14
+ attr_reader :serializers
15
+
16
+ # The configured {Compressor Compressor}.
17
+ # @return [Compressor] the configured compressor.
18
+ attr_accessor :compressor
19
+
20
+ # The configured root_builder, which should return a {Node Node} when
21
+ # called.
22
+ # @return [Proc<Node>] the configured root_builder.
23
+ attr_accessor :root_builder
24
+
25
+ attr_accessor :tmp_path
26
+
27
+ # Returns a new properties instance.
28
+ def initialize
29
+ reset
30
+ end
31
+
32
+ # Resets back to default properties.
33
+ def reset
34
+ reset_readers
35
+ reset_serializers
36
+
37
+ self.compressor = Rambling::Trie::Compressor.new
38
+ self.root_builder = lambda { Rambling::Trie::RawNode.new }
39
+ self.tmp_path = '/tmp'
40
+ end
41
+
42
+ private
43
+
44
+ attr_writer :readers, :serializers
45
+
46
+ def reset_readers
47
+ plain_text_reader = Rambling::Trie::Readers::PlainText.new
48
+
49
+ self.readers = Rambling::Trie::Configuration::ProviderCollection.new 'reader', txt: plain_text_reader
50
+ end
51
+
52
+ def reset_serializers
53
+ marshal_serializer = Rambling::Trie::Serializers::Marshal.new
54
+ yaml_serializer = Rambling::Trie::Serializers::Yaml.new
55
+ zip_serializer = Rambling::Trie::Serializers::Zip.new self
56
+
57
+ self.serializers = Rambling::Trie::Configuration::ProviderCollection.new 'serializer',
58
+ marshal: marshal_serializer,
59
+ yml: yaml_serializer,
60
+ yaml: yaml_serializer,
61
+ zip: zip_serializer
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,101 @@
1
+ module Rambling
2
+ module Trie
3
+ module Configuration
4
+ # Collection of configurable providers.
5
+ class ProviderCollection
6
+ extend Rambling::Trie::Forwardable
7
+
8
+ # The name of this provider collection.
9
+ # @return [String] the name of this provider collection.
10
+ attr_reader :name
11
+
12
+ # @overload default
13
+ # The default provider. Used when a provider cannot be resolved in
14
+ # {ProviderCollection#resolve #resolve}.
15
+ # @overload default=(provider)
16
+ # Sets the default provider. Needs to be one of the configured
17
+ # providers.
18
+ # @param [Object] provider the provider to use as default.
19
+ # @raise [ArgumentError] when the given provider is not in the
20
+ # provider collection.
21
+ # @note If no providers have been configured, `nil` will be assigned.
22
+ # @return [Object, nil] the default provider to use when a provider
23
+ # cannot be resolved in {ProviderCollection#resolve #resolve}.
24
+ attr_reader :default
25
+
26
+ delegate [
27
+ :[],
28
+ :[]=,
29
+ :keys,
30
+ :values,
31
+ ] => :providers
32
+
33
+ # Creates a new provider collection.
34
+ # @param [String] name the name for this provider collection.
35
+ # @param [Hash] providers the configured providers.
36
+ # @param [Object] default the configured default provider.
37
+ def initialize name, providers = {}, default = nil
38
+ @name = name
39
+ @configured_providers = providers
40
+ @configured_default = default || providers.values.first
41
+
42
+ reset
43
+ end
44
+
45
+ # Adds a new provider to the provider collection.
46
+ # @param [Symbol] extension the extension that the provider will
47
+ # correspond to.
48
+ # @param [provider] provider the provider to add to the provider
49
+ # collection.
50
+ def add extension, provider
51
+ providers[extension] = provider
52
+ end
53
+
54
+ def default= provider
55
+ if provider_not_in_list? provider
56
+ raise ArgumentError, "default #{name} should be part of configured #{name}s"
57
+ end
58
+
59
+ @default = provider
60
+ end
61
+
62
+ # List of configured providers.
63
+ # @return [Hash] the mapping of extensions to their corresponding
64
+ # providers.
65
+ def providers
66
+ @providers ||= {}
67
+ end
68
+
69
+ # Resolves the provider from a filepath based on the file extension.
70
+ # @param [String] filepath the filepath to resolve into a provider.
71
+ # @return [Object] the provider corresponding to the file extension in
72
+ # this provider collection. {#default} if not found.
73
+ def resolve filepath
74
+ providers[format filepath] || default
75
+ end
76
+
77
+ # Resets the provider collection to the initial values.
78
+ def reset
79
+ providers.clear
80
+ configured_providers.each { |k, v| providers[k] = v }
81
+ self.default = configured_default
82
+ end
83
+
84
+ private
85
+
86
+ attr_reader :configured_providers, :configured_default
87
+
88
+ def format filepath
89
+ format = File.extname filepath
90
+ format.slice! 0
91
+ format.to_sym
92
+ end
93
+
94
+ def provider_not_in_list? provider
95
+ (provider && providers.values.empty?) ||
96
+ (providers.values.any? && !providers.values.include?(provider))
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -1,6 +1,6 @@
1
1
  module Rambling
2
2
  module Trie
3
- # Wrapper on top of Trie data structure.
3
+ # Wrapper on top of trie data structure.
4
4
  class Container
5
5
  extend Rambling::Trie::Forwardable
6
6
  include ::Enumerable
@@ -25,32 +25,32 @@ module Rambling
25
25
  # @return [Node] the root node of this trie.
26
26
  attr_reader :root
27
27
 
28
- # Creates a new Trie.
28
+ # Creates a new trie.
29
29
  # @param [Node] root the root node for the trie
30
30
  # @param [Compressor] compressor responsible for compressing the trie
31
31
  # @yield [Container] the trie just created.
32
- def initialize root = nil, compressor = nil
33
- @root = root || default_root
34
- @compressor = compressor || default_compressor
32
+ def initialize root, compressor
33
+ @root = root
34
+ @compressor = compressor
35
35
 
36
36
  yield self if block_given?
37
37
  end
38
38
 
39
- # Adds a branch to the trie based on the word, without changing the passed word.
39
+ # Adds a word to the trie, without altering the passed word.
40
40
  # @param [String] word the word to add the branch from.
41
41
  # @return [Node] the just added branch's root node.
42
42
  # @raise [InvalidOperation] if the trie is already compressed.
43
43
  # @see RawNode#add
44
44
  # @see CompressedNode#add
45
- # @note Avoids clearing the contents of the word variable.
45
+ # @note Avoids altering the contents of the word variable.
46
46
  def add word
47
47
  root.add word.clone
48
48
  end
49
49
 
50
- # Compresses the existing tree using redundant node elimination. Flags
50
+ # Compresses the existing tree using redundant node elimination. Marks
51
51
  # the trie as compressed.
52
52
  # @return [Container] self
53
- # @note Avoids compressing again if the trie has already been compressed.
53
+ # @note Only compresses tries that have not already been compressed.
54
54
  def compress!
55
55
  self.root = compressor.compress root unless root.compressed?
56
56
  self
@@ -58,25 +58,61 @@ module Rambling
58
58
 
59
59
  # Checks if a path for a word or partial word exists in the trie.
60
60
  # @param [String] word the word or partial word to look for in the trie.
61
- # @return [Boolean] `true` if the word or partial word is found, `false` otherwise.
61
+ # @return [Boolean] `true` if the word or partial word is found, `false`
62
+ # otherwise.
63
+ # @see RawNode#partial_word?
64
+ # @see CompressedNode#partial_word?
62
65
  def partial_word? word = ''
63
66
  root.partial_word? word.chars
64
67
  end
65
68
 
66
69
  # Checks if a whole word exists in the trie.
67
70
  # @param [String] word the word to look for in the trie.
68
- # @return [Boolean] `true` only if the word is found and the last character corresponds to a terminal node.
71
+ # @return [Boolean] `true` only if the word is found and the last
72
+ # character corresponds to a terminal node, `false` otherwise.
73
+ # @see RawNode#word?
74
+ # @see CompressedNode#word?
69
75
  def word? word = ''
70
76
  root.word? word.chars
71
77
  end
72
78
 
73
79
  # Returns all words that start with the specified characters.
74
80
  # @param [String] word the word to look for in the trie.
75
- # @return [Array] all the words contained in the trie that start with the specified characters.
81
+ # @return [Array<String>] all the words contained in the trie that start
82
+ # with the specified characters.
83
+ # @see RawNode#scan
84
+ # @see CompressedNode#scan
76
85
  def scan word = ''
77
86
  root.scan(word.chars).to_a
78
87
  end
79
88
 
89
+ # Returns all words within a string that match a word contained in the
90
+ # trie.
91
+ # @param [String] phrase the string to look for matching words in.
92
+ # @return [Enumerator<String>] all the words in the given string that
93
+ # match a word in the trie.
94
+ # @yield [String] each word found in phrase.
95
+ # @see Node#words_within
96
+ def words_within phrase
97
+ words_within_root(phrase).to_a
98
+ end
99
+
100
+ # Checks if there are any valid words in a given string.
101
+ # @param [String] phrase the string to look for matching words in.
102
+ # @return [Boolean] `true` if any word within phrase is contained in the
103
+ # trie, `false` otherwise.
104
+ # @see Container#words_within
105
+ def words_within? phrase
106
+ words_within_root(phrase).any?
107
+ end
108
+
109
+ # Compares two trie data structures.
110
+ # @param [Container] other the trie to compare against.
111
+ # @return [Boolean] `true` if the tries are equal, `false` otherwise.
112
+ def == other
113
+ root == other.root
114
+ end
115
+
80
116
  alias_method :include?, :word?
81
117
  alias_method :match?, :partial_word?
82
118
  alias_method :words, :scan
@@ -87,12 +123,16 @@ module Rambling
87
123
  attr_reader :compressor
88
124
  attr_writer :root
89
125
 
90
- def default_root
91
- Rambling::Trie::RawNode.new
92
- end
126
+ def words_within_root phrase
127
+ return enum_for :words_within_root, phrase unless block_given?
93
128
 
94
- def default_compressor
95
- Rambling::Trie::Compressor.new
129
+ chars = phrase.chars
130
+ 0.upto(chars.length - 1).each do |starting_index|
131
+ new_phrase = chars.slice starting_index..(chars.length - 1)
132
+ root.match_prefix new_phrase do |word|
133
+ yield word
134
+ end
135
+ end
96
136
  end
97
137
  end
98
138
  end
@@ -1,6 +1,6 @@
1
1
  module Rambling
2
2
  module Trie
3
- # Provides enumerable behavior to the Trie data structure.
3
+ # Provides enumerable behavior to the trie data structure.
4
4
  module Enumerable
5
5
  include ::Enumerable
6
6
 
@@ -1,12 +1,17 @@
1
1
  module Rambling
2
2
  module Trie
3
- # Provides delegation behavior
3
+ # Provides delegation behavior.
4
4
  module Forwardable
5
- def delegate delegated_methods_to_delegated_to
6
- delegated_methods_to_delegated_to.each do |methods, delegated_to|
5
+ # Custom delegation behavior due to Ruby 2.4 delegation performance
6
+ # degradation. See {https://bugs.ruby-lang.org/issues/13111 Bug #13111}.
7
+ # @param [Hash] methods_to_target a Hash consisting of the methods to be
8
+ # delegated and the target to delegate those methods to.
9
+ # @return [Hash] the `methods_to_target` parameter.
10
+ def delegate methods_to_target
11
+ methods_to_target.each do |methods, target|
7
12
  methods.each do |method|
8
13
  define_method method do |*args|
9
- send(delegated_to).send method, *args
14
+ send(target).send method, *args
10
15
  end
11
16
  end
12
17
  end
@@ -0,0 +1,37 @@
1
+ module Rambling
2
+ module Trie
3
+ # Provides pretty printing behavior for the trie data structure.
4
+ module Inspectable
5
+ # @return [String] a string representation of the current node.
6
+ def inspect
7
+ "#<#{class_name} #{attributes}>"
8
+ end
9
+
10
+ private
11
+
12
+ def class_name
13
+ self.class.name
14
+ end
15
+
16
+ def attributes
17
+ [
18
+ letter_inspect,
19
+ terminal_inspect,
20
+ children_inspect,
21
+ ].join ', '
22
+ end
23
+
24
+ def letter_inspect
25
+ "letter: #{letter.inspect}"
26
+ end
27
+
28
+ def terminal_inspect
29
+ "terminal: #{terminal.inspect}"
30
+ end
31
+
32
+ def children_inspect
33
+ "children: #{children_tree.keys.inspect}"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,8 +1,9 @@
1
1
  module Rambling
2
2
  module Trie
3
- # Raised when trying to execute an invalid operation on a Trie data structure.
3
+ # Raised when trying to execute an invalid operation on a trie data
4
+ # structure.
4
5
  class InvalidOperation < Exception
5
- # Creates a new InvalidOperation exception.
6
+ # Creates a new {InvalidOperation InvalidOperation} exception.
6
7
  # @param [String, nil] message the exception message.
7
8
  def initialize message = nil
8
9
  super