diff --git a/.codeclimate.yml b/.codeclimate.yml index f871444b..5a299e2d 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,2 +1,33 @@ -exclude_paths: +exclude_patterns: - 'tasks/' +plugins: + # No to-dos or similar + fixme: + enabled: true + exclude_patterns: + - '.rubocop.*' + # ABC-complexity + flog: + enabled: true + config: + score_threshold: 25.0 + exclude_patterns: + - 'spec/' + # Markdown lint with rules from https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md + markdownlint: + enabled: true + # Code smells + reek: + enabled: true + exclude_patterns: + - 'spec/' + # Ruby lint + rubocop: + enabled: true + channel: rubocop-1-50-2 + # Semgrep Ruby rules + semgrep: + enabled: true + runs: + - configs: + - rules/ruby/lang diff --git a/.mdl_style.rb b/.mdl_style.rb new file mode 100644 index 00000000..60636ece --- /dev/null +++ b/.mdl_style.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +rule 'MD013', line_length: 120 +rule 'MD029', style: 'ordered' diff --git a/.mdlrc b/.mdlrc new file mode 100644 index 00000000..35400a10 --- /dev/null +++ b/.mdlrc @@ -0,0 +1 @@ +style '.mdl_style.rb' \ No newline at end of file diff --git a/.rubocop.yml b/.rubocop.yml index c07e7f66..c483e806 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -29,6 +29,37 @@ Layout/AccessModifierIndentation: Layout/ArgumentAlignment: EnforcedStyle: with_fixed_indentation +# checks whether the end keywords are aligned properly for `do` `end` blocks. +Layout/BlockAlignment: + # The value `start_of_block` means that the `end` should be aligned with line + # where the `do` keyword appears. + # The value `start_of_line` means it should be aligned with the whole + # expression's starting line. + # The value `either` means both are allowed. + EnforcedStyleAlignWith: start_of_line + +Layout/DefEndAlignment: + # The value `def` means that `end` should be aligned with the def keyword. + # The value `start_of_line` means that `end` should be aligned with method + # calls like `private`, `public`, etc, if present in front of the `def` + # keyword on the same line. + EnforcedStyleAlignWith: start_of_line + # AutoCorrect: false + Severity: warning + +# Align ends correctly. +Layout/EndAlignment: + # The value `keyword` means that `end` should be aligned with the matching + # keyword (`if`, `while`, etc.). + # The value `variable` means that in assignments, `end` should be aligned + # with the start of the variable on the left hand side of `=`. In all other + # situations, `end` should still be aligned with the keyword. + # The value `start_of_line` means that `end` should be aligned with the start + # of the line which the matching keyword appears on. + EnforcedStyleAlignWith: start_of_line + # AutoCorrect: false + Severity: warning + # Align the elements of a hash literal if they span more than one line. Layout/HashAlignment: # Alignment of entries using colon as separator. Valid values are: @@ -76,6 +107,23 @@ Layout/HashAlignment: # b: 2) EnforcedLastArgumentHashStyle: ignore_implicit +Layout/LineLength: + Max: 120 + # To make it possible to copy or click on URIs in the code, we allow lines + # containing a URI to be longer than Max. + AllowHeredoc: true + AllowURI: true + URISchemes: + - http + - https + # The IgnoreCopDirectives option causes the LineLength rule to ignore cop + # directives like '# rubocop: enable ...' when calculating a line's length. + IgnoreCopDirectives: false + # The IgnoredPatterns option is a list of !ruby/regexp and/or string + # elements. Strings will be converted to Regexp objects. A line that matches + # any regular expression listed in this option will be ignored by LineLength. + IgnoredPatterns: [] + Layout/ParameterAlignment: # Alignment of parameters in multi-line method calls. # @@ -1304,23 +1352,6 @@ Metrics/ClassLength: Metrics/CyclomaticComplexity: Max: 6 -Layout/LineLength: - Max: 80 - # To make it possible to copy or click on URIs in the code, we allow lines - # containing a URI to be longer than Max. - AllowHeredoc: true - AllowURI: true - URISchemes: - - http - - https - # The IgnoreCopDirectives option causes the LineLength rule to ignore cop - # directives like '# rubocop: enable ...' when calculating a line's length. - IgnoreCopDirectives: false - # The IgnoredPatterns option is a list of !ruby/regexp and/or string - # elements. Strings will be converted to Regexp objects. A line that matches - # any regular expression listed in this option will be ignored by LineLength. - IgnoredPatterns: [] - Metrics/MethodLength: CountComments: false # count full line comments? Max: 20 @@ -1331,6 +1362,7 @@ Metrics/ModuleLength: Metrics/ParameterLists: Max: 5 + MaxOptionalParameters: 5 CountKeywordArgs: true Metrics/PerceivedComplexity: @@ -1342,37 +1374,6 @@ Metrics/PerceivedComplexity: Lint/AssignmentInCondition: AllowSafeAssignment: true -# checks whether the end keywords are aligned properly for `do` `end` blocks. -Layout/BlockAlignment: - # The value `start_of_block` means that the `end` should be aligned with line - # where the `do` keyword appears. - # The value `start_of_line` means it should be aligned with the whole - # expression's starting line. - # The value `either` means both are allowed. - EnforcedStyleAlignWith: start_of_line - -Layout/DefEndAlignment: - # The value `def` means that `end` should be aligned with the def keyword. - # The value `start_of_line` means that `end` should be aligned with method - # calls like `private`, `public`, etc, if present in front of the `def` - # keyword on the same line. - EnforcedStyleAlignWith: start_of_line - # AutoCorrect: false - Severity: warning - -# Align ends correctly. -Layout/EndAlignment: - # The value `keyword` means that `end` should be aligned with the matching - # keyword (`if`, `while`, etc.). - # The value `variable` means that in assignments, `end` should be aligned - # with the start of the variable on the left hand side of `=`. In all other - # situations, `end` should still be aligned with the keyword. - # The value `start_of_line` means that `end` should be aligned with the start - # of the line which the matching keyword appears on. - EnforcedStyleAlignWith: start_of_line - # AutoCorrect: false - Severity: warning - Lint/SuppressedException: Exclude: - 'spec/**/*' diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c5300c0..276f98fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +# CHANGELOG + ## 2.3.2 [compare][compare_v2_3_1_and_master] ## 2.3.1 [compare][compare_v2_3_0_and_v2_3_1] @@ -60,7 +62,7 @@ - Add `Readers::Reader` and `Serializer::Serializer` base classes - Make all readers/serializers extend from their corresponding base classes - Better docs with `Reader`/`Serializer` and generics - - Fix all code blocks from `\`` to `+` and add some more + - Fix all code blocks from backtick to `+` and add some more - Add `@return [void]` where appropriate - Add `@return [self]` where appropriate - Fix `Nodes::Node` duplicate and broken references @@ -319,20 +321,15 @@ Most of these help with the gem's overall performance. - Add Ruby 2.4 to supported versions by [@gonzedge][github_user_gonzedge] - Drastically reduce size of gem by [@gonzedge][github_user_gonzedge] - - By excluding unnecessary `assets/` and `reports/` when building the gem. - **Size reduction**: from ~472KB to ~21KB. - + - By excluding unnecessary `assets/` and `reports/` when building the gem. + - **Size reduction**: from ~472KB to ~21KB. - Make root node accessible via container by [@gonzedge][github_user_gonzedge] - - So that anyone using rambling-trie can develop their custom algorithms - + - So that anyone using rambling-trie can develop their custom algorithms - Expose root node's `#to_a` method through `Container` by [@gonzedge][github_user_gonzedge] - Add own `Forwardable#delegate` because of [Ruby 2.4 performance degradation][ruby_bug_13111] by [@gonzedge][github_user_gonzedge] - - Was able to take Creation and Compression benchmarks (~8.8s and ~1.5s + - Was able to take Creation and Compression benchmarks (~8.8s and ~1.5s respectively) back down to the Ruby 2.3.3 levels by adding own definition of `Forwardable#delegate`. @@ -411,16 +408,12 @@ Most of these help with the gem's overall performance. - `Rambling::Trie.create` now returns a `Container` instead of a `Root` by [@gonzedge][github_user_gonzedge] - - `Container` exposes these API entry points: - + - `Container` exposes these API entry points: - `#partial_word?` and its alias `#match?` - `#word?` and its alias `#include?` - `#add` and its alias `#<<` - yield the constructed `Container` on `#initialize` - - `Rambling::Trie::Node` and its subclasses no longer expose: - + - `Rambling::Trie::Node` and its subclasses no longer expose: - `#match?` - `#include?` - `#<<` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 56220ef8..cd209722 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,15 @@ -## Contributing to Rambling Trie +# Contributing to Rambling Trie -1. If you have found a bug or have a feature request, please [search through the issues][github_issues_all] to see if it has already been reported. If that's not the case, then [create a new one][github_issues_new] with a full description of what you have found or what you need. -2. If you have bug fix or a feature implementation in mind, then [fork Rambling Trie][github_fork] and create a branch with a descriptive name. +1. If you have found a bug or have a feature request, please [search through the issues][github_issues_all] to see if it + has already been reported. If that's not the case, then [create a new one][github_issues_new] with a full description + of what you have found or what you need. +2. If you have bug fix or a feature implementation in mind, then [fork Rambling Trie][github_fork] and create a branch + with a descriptive name. 3. Get the gem up and running locally (tests are written in RSpec): ```sh bundle install - rake + bundle exec rake ``` 4. Implement your bug fix or feature - ***make sure to add tests!*** diff --git a/Gemfile b/Gemfile index f37dc915..c10f92b8 100644 --- a/Gemfile +++ b/Gemfile @@ -16,7 +16,7 @@ group :development do end group :test do - gem 'coveralls_reborn', '~> 0.27.0', require: false + gem 'coveralls_reborn', require: false gem 'rspec_junit_formatter' gem 'simplecov', require: false end diff --git a/README.md b/README.md index d56ac401..5acf6b46 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ [![Code Climate Grade][code_climate_grade_badge]][code_climate_link] [![Code Climate Issue Count][code_climate_issues_badge]][code_climate_link] -The Rambling Trie is a Ruby implementation of the [trie data structure][trie_wiki], which includes compression abilities and is designed to be very fast to traverse. +The Rambling Trie is a Ruby implementation of the [trie data structure][trie_wiki], which includes compression abilities +and is designed to be very fast to traverse. ## Installing the Rambling Trie @@ -57,7 +58,8 @@ Rambling::Trie.create do |trie| end ``` -Additionally, you can provide the path to a file that contains all the words to be added to the trie, and it will read the file and create the complete structure for you, like this: +Additionally, you can provide the path to a file that contains all the words to be added to the trie, and it will read +the file and create the complete structure for you, like this: ``` ruby trie = Rambling::Trie.create '/path/to/file' @@ -74,7 +76,10 @@ the trie ``` -If you want to use a custom file format, you will need to provide a custom file reader that defines an `#each_word` method that yields each word contained in the file. Look at the [`PlainText` reader][rambling_trie_plain_text_reader] class for an example, and at the [Configuration section][rambling_trie_configuration] to see how to add your own custom file readers. +If you want to use a custom file format, you will need to provide a custom `Reader` that defines an `#each_word` method +that yields each word contained in the file. Look at the [`PlainText` reader][rambling_trie_plain_text_reader] class for +an example, and at the [Configuration section][rambling_trie_configuration] to see how to add your own custom file +readers. ### Operations @@ -98,7 +103,8 @@ trie.word? 'word' trie.include? 'word' ``` -If you wish to find if part of a word exists in the trie instance, you should call `#partial_word?` or its alias `#match?`: +If you wish to find if part of a word exists in the trie instance, you should call `#partial_word?` or its +alias `#match?`: ``` ruby trie.partial_word? 'partial_word' @@ -119,7 +125,8 @@ trie.words_within 'ifdxawesome45someword3' # => ['if', 'aw', 'awe', ...] trie.words_within 'tktktktk' # => [] ``` -Or, if you're just interested in knowing whether a given string contains any valid words or not, you can use `#words_within?`: +Or, if you're just interested in knowing whether a given string contains any valid words or not, you can +use `#words_within?`: ``` ruby trie.words_within? 'ifdxawesome45someword3' # => true @@ -128,13 +135,15 @@ trie.words_within? 'tktktktk' # => false ### Compression -By default, the Rambling Trie works as a standard trie. Starting from version 0.1.0, you can obtain a compressed trie from the standard one, by using the compression feature. Just call the `#compress!` method on the trie instance: +By default, the Rambling Trie works as a standard trie. Starting from version 0.1.0, you can obtain a compressed trie +from the standard one, by using the compression feature. Just call the `#compress!` method on the trie instance: ``` ruby trie.compress! ``` -This will reduce the size of the trie by using redundant node elimination (redundant nodes are the only-child non-terminal nodes). +This will reduce the size of the trie by using redundant node elimination (redundant nodes are the only-child +non-terminal nodes). > _**Note**: The `#compress!` method acts over the trie instance it belongs to > and replaces the root `Node`. Also, adding words after compression (with `#add` or @@ -155,7 +164,8 @@ compressed_trie.compressed? # => true ### Enumeration -Starting from version 0.4.2, you can use any `Enumerable` method over a trie instance, and it will iterate over each word contained in the trie. You can now do things like: +Starting from version 0.4.2, you can use any `Enumerable` method over a trie instance, and it will iterate over each +word contained in the trie. You can now do things like: ``` ruby trie.each { |word| puts word } @@ -166,7 +176,10 @@ trie.all? { |word| word.include? 'x' } ### Serialization -Starting from version 1.0.0, you can store a full trie instance on disk and retrieve/use it later on. Loading a trie from disk takes less time, less cpu and less memory than loading every word into the trie every time. This is particularly useful for production applications, when you have word lists that you know are going to be static, or that change with little frequency. +Starting from version 1.0.0, you can store a full trie instance on disk and retrieve/use it later on. Loading a trie +from disk takes less time, less cpu and less memory than loading every word into the trie every time. This is +particularly useful for production applications, when you have word lists that you know are going to be static, or that +change with little frequency. To store a trie on disk, you can use `.dump` like this: @@ -174,7 +187,8 @@ To store a trie on disk, you can use `.dump` like this: Rambling::Trie.dump trie, '/path/to/file' ``` -Then, when you need to use a trie next time, you don't have to create a new one with all the necessary words. Rather, you can retrieve a previously stored one with `.load` like this: +Then, when you need to use a trie next time, you don't have to create a new one with all the necessary words. Rather, +you can retrieve a previously stored one with `.load` like this: ``` ruby trie = Rambling::Trie.load '/path/to/file' @@ -184,14 +198,15 @@ trie = Rambling::Trie.load '/path/to/file' Currently, these formats are supported to store tries on disk: -- Ruby's [binary (Marshal)][marshal] format -- [YAML][yaml] +* Ruby's [binary (Marshal)][marshal] format +* [YAML][yaml] > When dumping into or loading from disk, the format is determined > automatically based on the file extension, so `.yml` or `.yaml` files will be > handled through `YAML` and `.marshal` files through `Marshal`. -Optionally, you can use a `.zip` version of the supported formats. In order to do so, you'll have to install the [`rubyzip`][rubyzip] gem: +Optionally, you can use a `.zip` version of the supported formats. In order to do so, you'll have to install +the [`rubyzip`][rubyzip] gem: ``` bash gem install rubyzip @@ -246,7 +261,8 @@ end ### Further Documentation -You can find further API documentation on the autogenerated [rambling-trie gem RubyDoc.info page][rubydoc] or if you want edge documentation, you can go the [GitHub project RubyDoc.info page][rubydoc_github]. +You can find further API documentation on the autogenerated [rambling-trie gem RubyDoc.info page][rubydoc] or if you +want edge documentation, you can go the [GitHub project RubyDoc.info page][rubydoc_github]. ## Compatible Ruby and Rails versions @@ -271,7 +287,8 @@ The Rambling Trie has been tested with the following Ruby versions: ## Contributing to Rambling Trie -Take a look at the [contributing guide][rambling_trie_contributing_guide] to get started, or fire a question to [@gonzedge][github_user_gonzedge]. +Take a look at the [contributing guide][rambling_trie_contributing_guide] to get started, or fire a question +to [@gonzedge][github_user_gonzedge]. ## License and copyright @@ -279,11 +296,18 @@ Copyright (c) 2012-2023 Edgar González MIT License -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. [badge_fury_badge]: https://badge.fury.io/rb/rambling-trie.svg?version=2.3.1 [badge_fury_link]: https://badge.fury.io/rb/rambling-trie diff --git a/lib/rambling/trie.rb b/lib/rambling/trie.rb index ddef6274..b2357562 100644 --- a/lib/rambling/trie.rb +++ b/lib/rambling/trie.rb @@ -12,6 +12,7 @@ module Rambling # Entry point for +rambling-trie+ API. module Trie + # :reek:TooManyStatements { max_statements: 10 } class << self # Creates a new +Rambling::Trie+. Entry point for the +rambling-trie+ API. # @param [String, nil] filepath the file to load the words from. @@ -27,6 +28,7 @@ def create filepath = nil, reader = nil if filepath reader ||= readers.resolve filepath # noinspection RubyMismatchedArgumentType,RubyNilAnalysis + # :reek:NestedIterators reader.each_word filepath do |word| container << word end diff --git a/lib/rambling/trie/compressor.rb b/lib/rambling/trie/compressor.rb index 810442a3..297d5605 100644 --- a/lib/rambling/trie/compressor.rb +++ b/lib/rambling/trie/compressor.rb @@ -23,42 +23,21 @@ def compress_child_and_merge node def merge node, other letter = node.letter.to_s << other.letter.to_s - - new_compressed_node( - letter.to_sym, - node.parent, - other.children_tree, - other.terminal?, - ) + # :reek:FeatureEnvy + Rambling::Trie::Nodes::Compressed.new letter.to_sym, node.parent, other.children_tree, other.terminal? end def compress_children_and_copy node - new_compressed_node( - node.letter, - node.parent, - compress_children(node.children_tree), - node.terminal?, - ) + children_tree = compress_children node.children_tree + # :reek:FeatureEnvy + Rambling::Trie::Nodes::Compressed.new node.letter, node.parent, children_tree, node.terminal? end def compress_children tree new_tree = {} - - tree.each do |letter, child| - compressed_child = compress child - new_tree[letter] = compressed_child - end - + tree.each { |letter, child| new_tree[letter] = compress child } new_tree end - - def new_compressed_node letter, parent, tree, terminal - node = Rambling::Trie::Nodes::Compressed.new letter, parent, tree - node.terminal! if terminal - - tree.each_value { |child| child.parent = node } - node - end end end end diff --git a/lib/rambling/trie/configuration/properties.rb b/lib/rambling/trie/configuration/properties.rb index 1de51c90..a94f1754 100644 --- a/lib/rambling/trie/configuration/properties.rb +++ b/lib/rambling/trie/configuration/properties.rb @@ -4,6 +4,7 @@ module Rambling module Trie module Configuration # Provides configurable properties for Rambling::Trie. + # :reek:TooManyInstanceVariables { max_instance_variables: 5 } class Properties # The configured {Readers Readers}. # @return [ProviderCollection] the mapping of @@ -17,15 +18,18 @@ class Properties # The configured {Compressor Compressor}. # @return [Compressor] the configured compressor. + # :reek:Attribute attr_accessor :compressor # The configured +root_builder+, which returns a {Nodes::Node Node} # when called. # @return [Proc] the configured +root_builder+. + # :reek:Attribute attr_accessor :root_builder # The configured +tmp_path+, which will be used for throwaway files. # @return [String] the configured +tmp_path+. + # :reek:Attribute attr_accessor :tmp_path # Returns a new properties instance. @@ -35,6 +39,7 @@ def initialize # Resets back to default properties. # @return [void] + # :reek:TooManyStatements { max_statements: 10 } def reset reset_readers reset_serializers diff --git a/lib/rambling/trie/configuration/provider_collection.rb b/lib/rambling/trie/configuration/provider_collection.rb index c0fcd936..1c234e70 100644 --- a/lib/rambling/trie/configuration/provider_collection.rb +++ b/lib/rambling/trie/configuration/provider_collection.rb @@ -30,6 +30,7 @@ class ProviderCollection def initialize name, providers = {}, default = nil @name = name @configured_providers = providers + # :reek:ControlParameter @configured_default = default || providers.values.first reset @@ -66,14 +67,17 @@ def providers # @return [TProvider, nil] the provider for the given file's extension. # {#default} if not found. def resolve filepath - providers[file_format filepath] || default + format = File.extname filepath + # Cannot do slice(1..) because it returns +nil+ for empty strings + format.slice! 0 + providers[format.to_sym] || default end # Resets the provider collection to the initial values. # @return [void] def reset providers.clear - configured_providers.each { |k, v| self[k] = v } + configured_providers.each { |key, value| self[key] = value } self.default = configured_default end @@ -106,15 +110,9 @@ def values providers.values end - def file_format filepath - format = File.extname filepath - format.slice! 0 - format.to_sym - end - def contains? provider - provider.nil? || - (providers.any? && provider_instances.include?(provider)) + # :reek:ControlParameter + !provider || (providers.any? && provider_instances.include?(provider)) end alias_method :provider_instances, :values diff --git a/lib/rambling/trie/container.rb b/lib/rambling/trie/container.rb index da6c90e0..25e3c413 100644 --- a/lib/rambling/trie/container.rb +++ b/lib/rambling/trie/container.rb @@ -3,6 +3,7 @@ module Rambling module Trie # Wrapper on top of trie data structure. + # No such thing as :reek:TooManyMethods here because this is the API entrypoint. class Container include ::Enumerable @@ -202,8 +203,9 @@ def words_within_root phrase return enum_for :words_within_root, phrase unless block_given? chars = phrase.chars - 0.upto(chars.length - 1).each do |starting_index| - new_phrase = chars.slice starting_index..(chars.length - 1) + last_index = chars.length - 1 + 0.upto(last_index).each do |starting_index| + new_phrase = chars.slice starting_index..last_index root.match_prefix new_phrase do |word| yield word end @@ -215,9 +217,7 @@ def compress_root end def char_symbols word - symbols = [] - word.reverse.each_char { |c| symbols << c.to_sym } - symbols + word.reverse.chars.map(&:to_sym) end end end diff --git a/lib/rambling/trie/enumerable.rb b/lib/rambling/trie/enumerable.rb index 514a2bac..ebc10815 100644 --- a/lib/rambling/trie/enumerable.rb +++ b/lib/rambling/trie/enumerable.rb @@ -19,11 +19,8 @@ def each yield as_word if terminal? - children_tree.each_value do |child| - child.each do |word| - yield word - end - end + # :reek:NestedIterators + children_tree.each_value { |child| child.each { |word| yield word } } self end diff --git a/lib/rambling/trie/nodes/compressed.rb b/lib/rambling/trie/nodes/compressed.rb index 859dc729..0a980b99 100644 --- a/lib/rambling/trie/nodes/compressed.rb +++ b/lib/rambling/trie/nodes/compressed.rb @@ -4,7 +4,19 @@ module Rambling module Trie module Nodes # A representation of a node in an compressed trie data structure. + # :reek:RepeatedConditional + # :reek:TooManyStatements { max_statements: 10 } class Compressed < Rambling::Trie::Nodes::Node + # Creates a new compressed node. + # @param [Symbol, nil] letter the Node's letter value. + # @param [Node, nil] parent the parent of the current node. + # @param [Hash] children_tree the tree of child nodes. + # @param [Boolean, nil] terminal whether the node is terminal or not. + def initialize letter = nil, parent = nil, children_tree = {}, terminal = nil + super letter, parent, children_tree, terminal || nil + children_tree.each_value { |child| child.parent = self } + end + # Always raises {Rambling::Trie::InvalidOperation InvalidOperation} when # trying to add a word to the current compressed trie node # @param [String] _ the word to add to the trie. @@ -23,56 +35,62 @@ def compressed? private + # :reek:FeatureEnvy def partial_word_chars? chars child = children_tree[chars.first.to_sym] return false unless child child_letter = child.letter.to_s + size = child_letter.size - if chars.size >= child_letter.size - letter = chars.slice!(0, child_letter.size).join - return child.partial_word? chars if child_letter == letter + if chars.size >= size + partial_letter = chars.shift(size).join + return child.partial_word? chars if child_letter == partial_letter end - letter = chars.join - child_letter = child_letter.slice 0, letter.size - child_letter == letter + full_letter = chars.join + child_letter = child_letter.slice 0, full_letter.size + child_letter == full_letter end + # :reek:FeatureEnvy def word_chars? chars - letter = chars.slice! 0 + letter = chars.shift letter_sym = letter.to_sym child = children_tree[letter_sym] return false unless child + # :reek:DuplicateMethodCall loop do return child.word? chars if letter_sym == child.letter break if chars.empty? - letter << chars.slice!(0) + letter << chars.shift letter_sym = letter.to_sym end false end + # :reek:FeatureEnvy def closest_node chars child = children_tree[chars.first.to_sym] return missing unless child child_letter = child.letter.to_s + size = child_letter.size - if chars.size >= child_letter.size - letter = chars.slice!(0, child_letter.size).join - return child.scan chars if child_letter == letter + if chars.size >= size + partial_letter = chars.shift(size).join + return child.scan chars if child_letter == partial_letter end - letter = chars.join - child_letter = child_letter.slice 0, letter.size + full_letter = chars.join + child_letter = child_letter.slice 0, full_letter.size - child_letter == letter ? child : missing + child_letter == full_letter ? child : missing end def children_match_prefix chars @@ -84,7 +102,7 @@ def children_match_prefix chars return unless child child_letter = child.letter.to_s - letter = chars.slice!(0, child_letter.size).join + letter = chars.shift(child_letter.size).join return unless child_letter == letter diff --git a/lib/rambling/trie/nodes/node.rb b/lib/rambling/trie/nodes/node.rb index 46cfef54..19ce7f14 100644 --- a/lib/rambling/trie/nodes/node.rb +++ b/lib/rambling/trie/nodes/node.rb @@ -4,6 +4,9 @@ module Rambling module Trie module Nodes # A representation of a node in the trie data structure. + # :reek:RepeatedConditional + # :reek:TooManyMethods + # :reek:UtilityFunction { public_methods_only } class Node include Rambling::Trie::Compressible include Rambling::Trie::Enumerable @@ -24,19 +27,22 @@ class Node # Child nodes tree. # @return [Hash] the children tree hash, consisting of # +:letter => node+. + # :reek:Attribute attr_accessor :children_tree # Parent node. # @return [Node, nil] the parent of the current node. + # :reek:Attribute attr_accessor :parent # Creates a new node. # @param [Symbol, nil] letter the Node's letter value. # @param [Node, nil] parent the parent of the current node. - def initialize letter = nil, parent = nil, children_tree = {} + def initialize letter = nil, parent = nil, children_tree = {}, terminal = nil @letter = letter @parent = parent @children_tree = children_tree + @terminal = terminal end # Child nodes. @@ -71,6 +77,7 @@ def terminal? # Mark {Node Node} as terminal. # @return [self] the modified node. + # :reek:MissingSafeMethod def terminal! self.terminal = true self @@ -176,6 +183,7 @@ def missing private + # :reek:Attribute attr_accessor :terminal end end diff --git a/lib/rambling/trie/nodes/raw.rb b/lib/rambling/trie/nodes/raw.rb index 95f8a984..a9820f7f 100644 --- a/lib/rambling/trie/nodes/raw.rb +++ b/lib/rambling/trie/nodes/raw.rb @@ -4,6 +4,8 @@ module Rambling module Trie module Nodes # A representation of a node in an uncompressed trie data structure. + # :reek:RepeatedConditional + # :reek:TooManyStatements { max_statements: 10 } class Raw < Rambling::Trie::Nodes::Node # Adds a word to the current raw (uncompressed) trie node. # @param [Array] chars the char array to add to the trie. diff --git a/lib/rambling/trie/readers/reader.rb b/lib/rambling/trie/readers/reader.rb index b08f2ba0..ec98b845 100644 --- a/lib/rambling/trie/readers/reader.rb +++ b/lib/rambling/trie/readers/reader.rb @@ -12,6 +12,7 @@ class Reader # from. # @yield [String] Each line read from the file. # @return [self] + # :reek:UnusedParameter def each_word filepath raise NotImplementedError end diff --git a/lib/rambling/trie/serializers/file.rb b/lib/rambling/trie/serializers/file.rb index 5fc6db22..18ae1f3e 100644 --- a/lib/rambling/trie/serializers/file.rb +++ b/lib/rambling/trie/serializers/file.rb @@ -8,6 +8,7 @@ class File < Serializer # Loads contents from a specified filepath. # @param [String] filepath the filepath to load contents from. # @return [String] all contents of the file. + # :reek:UtilityFunction def load filepath ::File.read filepath end @@ -16,9 +17,10 @@ def load filepath # @param [String] contents the contents to dump. # @param [String] filepath the filepath to dump the contents to. # @return [Numeric] number of bytes written to disk. + # :reek:UtilityFunction def dump contents, filepath - ::File.open filepath, 'w+' do |f| - f.write contents + ::File.open filepath, 'w+' do |file| + file.write contents end end end diff --git a/lib/rambling/trie/serializers/marshal.rb b/lib/rambling/trie/serializers/marshal.rb index e1fb5d52..0330c707 100644 --- a/lib/rambling/trie/serializers/marshal.rb +++ b/lib/rambling/trie/serializers/marshal.rb @@ -9,8 +9,9 @@ class Marshal < Serializer # @param [Serializer] serializer the serializer responsible to write to # and read from disk. def initialize serializer = nil - @serializer = serializer || Rambling::Trie::Serializers::File.new super() + # :reek:ControlParameter + @serializer = serializer || Rambling::Trie::Serializers::File.new end # Loads marshaled object from contents in filepath and deserializes it diff --git a/lib/rambling/trie/serializers/serializer.rb b/lib/rambling/trie/serializers/serializer.rb index f9f961d4..81265222 100644 --- a/lib/rambling/trie/serializers/serializer.rb +++ b/lib/rambling/trie/serializers/serializer.rb @@ -9,6 +9,7 @@ class Serializer # @abstract Subclass and override {#load} to parse the desired format. # @param [String] filepath the filepath to load contents from. # @return [TContents] parsed contents from given file. + # :reek:UnusedParameter def load filepath raise NotImplementedError end @@ -18,6 +19,7 @@ def load filepath # @param [TContents] contents the contents to dump into given file. # @param [String] filepath the filepath to dump the contents to. # @return [Numeric] number of bytes written to disk. + # :reek:UnusedParameter def dump contents, filepath raise NotImplementedError end diff --git a/lib/rambling/trie/serializers/yaml.rb b/lib/rambling/trie/serializers/yaml.rb index cadd4cb2..adeaeb79 100644 --- a/lib/rambling/trie/serializers/yaml.rb +++ b/lib/rambling/trie/serializers/yaml.rb @@ -9,8 +9,9 @@ class Yaml < Serializer # @param [Serializer] serializer the serializer responsible to write to # and read from disk. def initialize serializer = nil - @serializer = serializer || Rambling::Trie::Serializers::File.new super() + # :reek:ControlParameter + @serializer = serializer || Rambling::Trie::Serializers::File.new end # Loads serialized object from YAML file in filepath and deserializes diff --git a/lib/rambling/trie/serializers/zip.rb b/lib/rambling/trie/serializers/zip.rb index 8c01f96b..d87389c9 100644 --- a/lib/rambling/trie/serializers/zip.rb +++ b/lib/rambling/trie/serializers/zip.rb @@ -6,13 +6,15 @@ module Serializers # Zip file serializer. Dumps/loads contents from +.zip+ files. # Automatically detects if zip file contains a +.marshal+ or +.yml+ file, # or any other registered +:format => serializer+ combo. + # :reek:TooManyStatements { max_statements: 10 } class Zip < Serializer # Creates a new Zip serializer. # @param [Configuration::Properties] properties the configuration # properties set up so far. def initialize properties - @properties = properties super() + # :reek:ControlParameter + @properties = properties end # Unzip contents from specified filepath and load in contents from @@ -26,10 +28,11 @@ def load filepath ::Zip::File.open filepath do |zip| entry = zip.entries.first - entry_path = path entry.name + entry_name = entry.name + entry_path = path entry_name entry.extract entry_path - serializer = serializers.resolve entry.name + serializer = serializers.resolve entry_name serializer.load entry_path end end diff --git a/spec/integration/rambling/trie_spec.rb b/spec/integration/rambling/trie_spec.rb index df07afbe..c578c5a3 100644 --- a/spec/integration/rambling/trie_spec.rb +++ b/spec/integration/rambling/trie_spec.rb @@ -13,24 +13,28 @@ let(:changelog_path) { File.join root_path, 'CHANGELOG.md' } let(:changelog) { File.read changelog_path } + let(:changelog_versions) do + matches = [] + changelog.scan %r{^## (\d+\.\d+\.\d+)} do |match| + matches << match[0] + end + matches + end + it 'matches with the version in the README badge' do match = %r{\?version=(?.*)$}.match readme expect(match['version']).to eq Rambling::Trie::VERSION end it 'is the version before the one at the top of the CHANGELOG' do - match = %r{## (?\d+\.\d+\.\d+)}.match changelog.split("\n")[0] - changelog_version = Gem::Version.new match['version'] + changelog_version = Gem::Version.new changelog_versions.first lib_version = Gem::Version.new "#{Rambling::Trie::VERSION}.0" expect(changelog_version).to eq lib_version.bump end it 'is included in the CHANGELOG diffs' do - matches = Set.new - changelog.scan %r{^## (\d+\.\d+\.\d+)} do |match| - matches << match[0] - end - expect(matches).to include Rambling::Trie::VERSION + changelog_versions.shift + expect(changelog_versions.first).to eq Rambling::Trie::VERSION end end diff --git a/tasks/ips.rb b/tasks/ips.rb index 5ea1142c..6c1a41be 100644 --- a/tasks/ips.rb +++ b/tasks/ips.rb @@ -1,6 +1,14 @@ # frozen_string_literal: true namespace :ips do + task :each_char_shovel_vs_chars_map do + compare_each_char_shovel_vs_chars_map + end + + task :string_pop_shift_slice do + compare_string_pop_shift_slice + end + task :pop_shift_slice do compare_pop_shift_slice end @@ -39,15 +47,49 @@ def compare end end +def compare_each_char_shovel_vs_chars_map + compare do |bm| + word = 'awesome' + + bm.report 'each_char and <<' do + symbols = [] + word.reverse.each_char { |char| symbols << char.to_sym } + symbols.to_a + end + + bm.report 'chars map' do + word.reverse.chars.map(&:to_sym).to_a + end + end +end + +def compare_string_pop_shift_slice + compare do |bm| + a = '' + bm.report('<<') { a << 'a' } + bm.report('pop') { a.chars.pop } + + a.clear + bm.report('<<') { a << 'a' } + bm.report('shift') { a.chars.shift } + + a.clear + bm.report('<<') { a << 'a' } + bm.report('slice!(0)') { a.slice! 0 } + end +end + def compare_pop_shift_slice compare do |bm| a = [] bm.report('push') { a.push 1 } bm.report('pop') { a.pop } + a.clear bm.report('unshift') { a.unshift 1 } bm.report('shift') { a.shift } + a.clear bm.report('shovel(<<)') { a << 1 } bm.report('slice!(0)') { a.slice! 0 } end