collatz 0.0.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac3403918c8be4b788fdfab2ec298d657c29eb05437c01652f9dd91387a61290
4
- data.tar.gz: 67c1ad526601163e3356d0b2318710ff9e2b7691696e08c511fe71e029d83d16
3
+ metadata.gz: 30d4085b4e190a4e1aaf4fdd5f4108abb26688c8223bc1725091d6976b257252
4
+ data.tar.gz: 3467c2848b02d28909455f092759a8cad3e67eba18583c3873e2bf74c456b11c
5
5
  SHA512:
6
- metadata.gz: 5987d1636fb13b32ab6f886b4c9223eb747584e43bae7476d5ec69d7ee5515d2d38ff658ccb902690fd87442f6c7ec608c4c02d078feb134a196bd2b43d101fb
7
- data.tar.gz: e0ff96c1119cc04ec3cd9c235830436e6b1dabb80b1188b731bd90eee5168d31fb3aedfa136dfdfe3b5e554a0b8afd5c80c9f1c7c81b0727f90ef5c046b01a8b
6
+ metadata.gz: 9e6d2e859b8cc330406d54989b4a01ee1e83d7e7da1ffad2d66b78eb588aa19e8bcd7b504bd4aeb8320710ae69eb1e2f9d6762a33a5b9b8532a2a91e0190e5f4
7
+ data.tar.gz: df4b8fa5f25bbc86664ea45d012db4deb07c1197c8addb5fb2f8cb3c22354a89e1612a3b0eb362c8c3bacf2c9b8792a4ba7f746fb40d43ba30066b4aebcd20d8
data/.rubocop.yml CHANGED
@@ -1,10 +1,9 @@
1
1
  AllCops:
2
2
  TargetRubyVersion: 2.7.0
3
3
  UseCache: false
4
+ SuggestExtensions: false
4
5
  # NewCops: enable # would silence the recommendation
5
6
  # # to enable new, but would hide which ones were run
6
- # SuggestExtensions: false # would silence the tips
7
- # # for recommended suggested gems.
8
7
 
9
8
  Gemspec/DeprecatedAttributeAssignment:
10
9
  Enabled: true
@@ -13,12 +12,16 @@ Gemspec/RequireMFA:
13
12
 
14
13
  Naming/BlockForwarding:
15
14
  Enabled: true
15
+ Naming/MethodParameterName:
16
+ MinNameLength: 1
16
17
 
17
18
  Security/CompoundHash:
18
19
  Enabled: true
19
20
  Security/IoMethods:
20
21
  Enabled: true
21
22
 
23
+ Layout/EmptyLineAfterGuardClause:
24
+ Enabled: false
22
25
  Layout/LineLength:
23
26
  Max: 120
24
27
  Layout/LineContinuationLeadingSpace:
@@ -27,9 +30,28 @@ Layout/LineContinuationSpacing:
27
30
  Enabled: true
28
31
  Layout/LineEndStringConcatenationIndentation:
29
32
  Enabled: true
33
+ Layout/MultilineHashBraceLayout:
34
+ Enabled: false
35
+ Layout/SpaceAroundOperators:
36
+ Enabled: false
30
37
  Layout/SpaceBeforeBrackets:
31
38
  Enabled: true
32
39
 
40
+ Metrics/AbcSize:
41
+ Enabled: false
42
+ Metrics/BlockLength:
43
+ AllowedMethods: ['describe', 'context']
44
+ Metrics/BlockNesting:
45
+ Enabled: false
46
+ Metrics/CyclomaticComplexity:
47
+ Enabled: false
48
+ Metrics/MethodLength:
49
+ Enabled: false
50
+ Metrics/ParameterLists:
51
+ Enabled: false
52
+ Metrics/PerceivedComplexity:
53
+ Enabled: false
54
+
33
55
  Lint/AmbiguousAssignment:
34
56
  Enabled: true
35
57
  Lint/AmbiguousOperatorPrecedence:
@@ -92,10 +114,15 @@ Style/StringLiterals:
92
114
  Style/StringLiteralsInInterpolation:
93
115
  Enabled: true
94
116
  EnforcedStyle: double_quotes
117
+ Style/AccessModifierDeclarations:
118
+ EnforcedStyle: inline
119
+ AllowModifiersOnSymbols: false
95
120
  Style/ArgumentsForwarding:
96
121
  Enabled: true
97
122
  Style/CollectionCompact:
98
123
  Enabled: true
124
+ Style/ConditionalAssignment:
125
+ Enabled: false
99
126
  Style/DocumentDynamicEvalDefinition:
100
127
  Enabled: true
101
128
  Style/EmptyHeredoc:
@@ -110,6 +137,8 @@ Style/FileRead:
110
137
  Enabled: true
111
138
  Style/FileWrite:
112
139
  Enabled: true
140
+ Style/For:
141
+ Enabled: false
113
142
  Style/HashConversion:
114
143
  Enabled: true
115
144
  Style/HashExcept:
@@ -118,6 +147,8 @@ Style/IfWithBooleanLiteralBranches:
118
147
  Enabled: true
119
148
  Style/InPatternThen:
120
149
  Enabled: true
150
+ Style/Lambda:
151
+ EnforcedStyle: lambda
121
152
  Style/MagicCommentFormat:
122
153
  Enabled: true
123
154
  Style/MapCompactWithConditionalBlock:
@@ -130,12 +161,16 @@ Style/NegatedIfElseCondition:
130
161
  Enabled: true
131
162
  Style/NestedFileDirname:
132
163
  Enabled: true
164
+ Style/Next:
165
+ Enabled: false
133
166
  Style/NilLambda:
134
167
  Enabled: true
135
168
  Style/NumberedParameters:
136
169
  Enabled: true
137
170
  Style/NumberedParametersLimit:
138
171
  Enabled: true
172
+ Style/NumericLiterals:
173
+ Enabled: false
139
174
  Style/ObjectThen:
140
175
  Enabled: true
141
176
  Style/OpenStructUse:
@@ -146,6 +181,8 @@ Style/RedundantArgument:
146
181
  Enabled: true
147
182
  Style/RedundantInitialize:
148
183
  Enabled: true
184
+ Style/RedundantSelf:
185
+ Enabled: false
149
186
  Style/RedundantSelfAssignmentBranch:
150
187
  Enabled: true
151
188
  Style/SelectByRegexp:
data/Gemfile CHANGED
@@ -7,6 +7,8 @@ gemspec
7
7
 
8
8
  gem "rake", "~> 13.0"
9
9
 
10
+ gem "rdoc", "~> 6.0"
11
+
10
12
  gem "rspec", "~> 3.0"
11
13
 
12
14
  gem "rubocop", "~> 1.21"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- collatz (0.0.4)
4
+ collatz (1.0.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -12,8 +12,12 @@ GEM
12
12
  parallel (1.22.1)
13
13
  parser (3.1.2.1)
14
14
  ast (~> 2.4.1)
15
+ psych (4.0.5)
16
+ stringio
15
17
  rainbow (3.1.1)
16
18
  rake (13.0.6)
19
+ rdoc (6.4.0)
20
+ psych (>= 4.0.0)
17
21
  regexp_parser (2.5.0)
18
22
  rexml (3.2.5)
19
23
  rspec (3.11.0)
@@ -42,6 +46,7 @@ GEM
42
46
  rubocop-ast (1.21.0)
43
47
  parser (>= 3.1.1.0)
44
48
  ruby-progressbar (1.11.0)
49
+ stringio (3.0.2)
45
50
  unicode-display_width (2.2.0)
46
51
 
47
52
  PLATFORMS
@@ -50,6 +55,7 @@ PLATFORMS
50
55
  DEPENDENCIES
51
56
  collatz!
52
57
  rake (~> 13.0)
58
+ rdoc (~> 6.0)
53
59
  rspec (~> 3.0)
54
60
  rubocop (~> 1.21)
55
61
 
data/Makefile CHANGED
@@ -1,4 +1,4 @@
1
- .PHONY: setup setup_github clean test build install push_rubygems push_github
1
+ .PHONY: setup setup_github clean docs test build install push_rubygems push_github
2
2
  SHELL:=/bin/bash
3
3
 
4
4
  # Assumes `gem install bundler`
@@ -10,11 +10,17 @@ setup_github:
10
10
 
11
11
  clean:
12
12
  bundle exec rake clean
13
+ bundle exec rake clobber
13
14
  rm -f collatz-*.gem
14
15
  rm -f pkg/collatz-*.gem
15
16
 
17
+ docs: clean
18
+ bundle exec rake rdoc
19
+
20
+ # default (just `rake`) is spec + rubocop, but be pedantic in case this changes.
16
21
  test: clean
17
- bundle exec rake
22
+ bundle exec rake spec
23
+ bundle exec rake rubocop
18
24
 
19
25
  # We can choose from `gem build collatz.gemspec` or `bundle exec rake build`.
20
26
  # The gem build command creates a ./collatz-$VER.gem file, and the rake build
data/README.md CHANGED
@@ -1,20 +1,37 @@
1
1
  # [Collatz](https://github.com/Skenvy/Collatz): [Ruby](https://github.com/Skenvy/Collatz/tree/main/ruby) 🔻💎🔻
2
2
  Functions related to [the Collatz/Syracuse/3N+1 problem](https://en.wikipedia.org/wiki/Collatz_conjecture), implemented in [Ruby](https://www.ruby-lang.org/).
3
3
  ## Getting Started
4
- [To install the latest from RubyGems](https://rubygems.org/); ([empty search results for collatz](https://rubygems.org/search?query=collatz))
4
+ [To install the latest from RubyGems](https://rubygems.org/gems/collatz);
5
5
  ```sh
6
6
  gem install collatz
7
7
  ```
8
+ [Or to install from GitHub's hosted gems](https://github.com/Skenvy/Collatz/packages/1636643);
9
+ ```sh
10
+ gem install collatz --source "https://rubygems.pkg.github.com/skenvy"
11
+ ```
12
+ ### Add to the Gemfile
13
+ [Add the RubyGems hosted gem](https://rubygems.org/gems/collatz);
14
+ ```ruby
15
+ gem "collatz", ">= 0.1.0
16
+ ```
17
+ [Add the GitHub hosted gem](https://github.com/Skenvy/Collatz/packages/1636643);
18
+ ```ruby
19
+ source "https://rubygems.pkg.github.com/skenvy" do
20
+ gem "collatz", ">= 0.1.0"
21
+ end
22
+ ```
8
23
  ## Usage
9
24
  Provides the basic functionality to interact with the Collatz conjecture.
10
25
  The parameterisation uses the same `(P,a,b)` notation as Conway's generalisations.
11
26
  Besides the function and reverse function, there is also functionality to retrieve the hailstone sequence, the "stopping time"/"total stopping time", or tree-graph.
12
27
  The only restriction placed on parameters is that both `P` and `a` can't be `0`.
13
- ## [<lang-docs-name> generated docs](https://skenvy.github.io/Collatz/ruby)
28
+ ## [RDoc generated docs](https://skenvy.github.io/Collatz/ruby)
14
29
  ## Developing
15
30
  ### The first time setup
16
31
  ```sh
17
- git clone https://github.com/Skenvy/Collatz.git && cd Collatz/ruby && <localised-setup-command>
32
+ git clone https://github.com/Skenvy/Collatz.git && cd Collatz/ruby && make setup
18
33
  ```
19
34
  ### Iterative development
20
- * <list-worthwhile-recipes>
35
+ The majority of `make` recipes for this are just wrapping a `bundle` invocation of `rake`.
36
+ * `make docs` will recreate the RDoc docs
37
+ * `make test` will run both the RSpec tests and the RuboCop linter.
data/Rakefile CHANGED
@@ -10,3 +10,26 @@ require "rubocop/rake_task"
10
10
  RuboCop::RakeTask.new
11
11
 
12
12
  task default: %i[spec rubocop]
13
+
14
+ # The below recommended way to add rdoc to rake from rdoc's site has a lot of
15
+ # things it will compain about, and wont work OotB, so use 'rdoc/task' below.
16
+ # require 'rdoc/rdoc'
17
+ # options = RDoc::Options.new
18
+ # # see RDoc::Options
19
+ # options.rdoc_include << ["lib/*.rb"]
20
+ # rdoc = RDoc::RDoc.new
21
+ # rdoc.document options
22
+ # # see RDoc::RDoc
23
+
24
+ # https://ruby.github.io/rdoc/RDocTask.html
25
+ # doc is the default output location of the rdoc binary, and is the location
26
+ # added in the standard ruby gitignore, but the rake task defaults to ./html
27
+ # and we need to use the rdoc_dir option to change it to doc.
28
+
29
+ require "rdoc/task"
30
+
31
+ RDoc::Task.new do |rdoc|
32
+ rdoc.rdoc_dir = "doc"
33
+ rdoc.main = "README.md" # !? README.rdoc ?!
34
+ rdoc.rdoc_files.include("README.md", "lib/*.rb", "lib/collatz/*.rb")
35
+ end
data/collatz.gemspec CHANGED
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
14
14
  Besides the function and reverse function, there is also functionality to retrieve
15
15
  the hailstone sequence, the \"stopping time\"/\"total stopping time\", or tree-graph.
16
16
  The only restriction placed on parameters is that both P and a can't be 0."
17
- spec.homepage = "https://github.com/Skenvy/Collatz/tree/main/ruby"
17
+ spec.homepage = "https://skenvy.github.io/Collatz/ruby/"
18
18
  spec.required_ruby_version = ">= 2.7.0"
19
19
  spec.metadata["homepage_uri"] = spec.homepage
20
20
  spec.metadata["source_code_uri"] = "https://github.com/Skenvy/Collatz/tree/main/ruby"
data/devlog.md CHANGED
@@ -53,3 +53,88 @@ While the [GitHub: Working with the RubyGems registry](https://docs.github.com/e
53
53
  :github: Bearer <PAT|TOKEN>
54
54
  ```
55
55
  We can `base64 ~/.gem/credentials` the rubygems_api_key we got from the `gem signin`, add the string (without the newlines) to our github secrets, and then do something like `echo -n "$RUBYGEMS_API_KEY_BASE64" | base64 --decode > ~/.gem/credentials` to recreate the credentials. GitHub's credentials should be passable with a `echo -e "---\n:github: Bearer <PAT|TOKEN>" > ~/.gem/credentials`.
56
+
57
+ With that now merged we can work on adding the functionality and setting up documentation. The two main choices for documentation generation appear to be [rdoc](https://github.com/ruby/rdoc) or [yard](https://yardoc.org/). Yard looks alright, but we're trying to stick to any "official" way of doing something, and with rdoc being put out by ruby, we'll start off with rdoc. We can [`bundle add rdoc --version "~> 6.0" --verbose`](https://bundler.io/man/bundle-add.1.html), but doing this just leads to it permanently hanging on a ""HTTP GET https://index.rubygems.org/versions" immediately after a "HTTP 416 Range Not Satisfiable https://index.rubygems.org/versions". There's quite a few hits for this or similar issues cropping up, but none with a definitive answer or cause, and none that worked to fix the issue here. It took a 20 minute wait for it to finally do anything, then kept asking for my sudo password and refusing to accept it. I retried and it succeeded much quicker, although still took 4 entries of my password to get it to successfully pass and hit that "Bundle complete! 5 Gemfile dependencies, 23 gems now installed". Following the [rdoc](https://github.com/ruby/rdoc) guide to add rdoc to the rakefile leads to a ``rake aborted! NoMethodError: undefined method `delete_if' for nil:NilClass`` which could be connected to [this issue](https://github.com/ruby/rdoc/issues/352), namely ["You haven't told RDoc to document any files"](https://github.com/ruby/rdoc/issues/352#issuecomment-110185993). We can avoid this issue if we use the [rdoc task](https://ruby.github.io/rdoc/RDocTask.html) instead.
58
+
59
+ As we start to add the first few lines to the main rb file, we've run into yet another rubocop yelling about something inconsequential (Style/NumericLiterals isn't even listed in our rubocop yaml, so we'll have to add it to disable it);
60
+ ```
61
+ lib/collatz.rb:19:20: C: [Correctable] Style/NumericLiterals: Use underscores(_) as thousands separator and separate every 3 digits with them.
62
+ VERIFIED_MAXIMUM = 295147905179352825856
63
+ ^^^^^^^^^^^^^^^^^^^^^
64
+ ```
65
+ We'll also have to add the cop `Naming/MethodParameterName: MinNameLength: 1` to stop it from complaining about all our uses of `"P"`, `"a"`, and `"b"`. Also worth mentioning is that, unlike other implementations, in ruby we can't use a capital letter to start a non-constant value, so we have to swap all capital `"P"` for lowercase `"p"`'s, lest we get the `"Formal argument cannot be a constant"` error. I'm also cautious of `Style/RedundantReturn` -- I'd like to try and stick to the normal ruby style, but it relies on the novel awareness that the last line of a function without an explicit return is returned. I'll have to simply remember that for later. Another cop is complaining about `Lint/AmbiguousOperatorPrecedence: Wrap expressions with varying precedence with parentheses to avoid ambiguity.`, which seems absurd. After fixing some of these, my line `if n%p == 0 then n/p else ((a*n)+b) end` is still generating the following errors;
66
+ ```
67
+ lib/collatz.rb:96:3: C: [Correctable] Style/OneLineConditional: Favor the ternary operator (?:) or multi-line constructs over single-line if/then/else/end constructs.
68
+ if n%p == 0 then n/p else ((a*n)+b) end
69
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
70
+ lib/collatz.rb:96:6: C: [Correctable] Style/NumericPredicate: Use (n%p).zero? instead of n%p == 0.
71
+ if n%p == 0 then n/p else ((a*n)+b) end
72
+ ^^^^^^^^
73
+ ```
74
+ Rubocop is happy if we replace it with `(n%p).zero? ? (n/p) : ((a*n)+b)`, which feels so much less readable, and absurd than anyone would consider that more "readable" than the verbose `if/then/else/end`. Another odd ambiguity is that an error can be raised either with `raise StandardError, 'message'` or `raise StandardError.new('message')` or `raise StandardError.new 'message'`, but the `Style/RaiseArgs` cop has a preference for `raise StandardError, 'message'`. Now we just have to configure the `Metrics/BlockLength` cop which doesn't like all the lines in the spec test. A good suggestion is to set `IgnoredMethods: ['describe', 'context']` according to the docs, which does silence the error, but now returns ``Warning: obsolete parameter `IgnoredMethods` (for `Metrics/BlockLength`) found in .rubocop.yml `IgnoredMethods` has been renamed to `AllowedMethods` and/or `AllowedPatterns`.``. So why are they not mentioned as such on the [rubocop docs](https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocklength). It feels like a lot of the problems we've run into so far with ruby have been the same as those with go, where it's been historically developed at a pace greater than which it is then adopted, leading to a fragmented ecosystem, although not as badly as it is in go, with solutions to circumvent the at times antithetical expectations easier to find than they were for go. Although a large portion of these issues have been with rubocop. It might be useful, but it adds a lot of noise that needs to be hushed while trying to learn ruby for the first time, even if its suggestions might be temporarily useful. I will say though that setting up the gem deployment was much easier than the maven deployment for java, so all in all it's not too much of a hassle.
75
+
76
+ The last thing worth mentioning before trying to release 0.1.0 with the function and reverse function, is that there seems to be a bit of a difference in how to provide "kwargs" than in python. In python kwargs are listed as `some_arg_name = some_default_value`, and can then be referenced by name in the function invocation, or supplied positionally, and without specifying a `**kwargs` to splatter additionl inputs, any name supplied that is not specified as an input will cause an error. In ruby, kwargs are listed as `some_arg_name: some_default_value`, and like python, an error will be thrown if a named kwarg is provided on invocation that isn't specified in the function inputs, however it **can only** be supplied to the invocation per its keyword name, it can't be supplied positionally. Meanwhile, "optional" args in ruby that are specified in the function inputs via `some_arg_name = some_default_value` aren't kwargs, but rather positional args that happen to have default values; they can't be supplied to a function invocation by name, only by position, and a quirk of testing how to assign them to learn the difference between ruby and python demonstrates that while specifying `**kwargs` to splatter inputs in python means parameters can be supplied to the invocation by name that weren't specified in the input, attempting to see how "optional args" affect splattering in ruby leads to a false positive of demonstrating something akin to, but not exactly splattering. That is, if some `some_other_arg_name = some_other_value` is supplied to a function invocation in ruby as the input to an "optional arg", this is not splatting the differently named inputs, rather, it is assigning the value to the name as a variable, and providing that value as the input to the positional optional args. So to achieve true "kwargs" in ruby, we need to use `some_arg_name: some_default_value`, and specify them by name in the invocations.
77
+
78
+ We're now ready to add once again create an empty orphan branch;
79
+ 1. `git checkout --orphan gh-pages-ruby`
80
+ 1. `rm .git/index ; git clean -fdx`
81
+ 1. `git commit -m "Initial empty orphan" --allow-empty`
82
+ 1. `git push --set-upstream origin gh-pages-ruby`
83
+
84
+ Well, v0.1.0 has been released, with the function and reverse function. The ruby discord has been very helpful, and has a "Community: code review" channel dedicated for requests for feedback on code samples, so I've asked on there for a review, and gotten some feedback! I want to try and make it more ruby-esque before working on the hailstone and tree graph functions, so that they are made right from the start. The feedback is as thus;
85
+ 1. It's odd to have all the code in the central `~/collatz.rb` file with a separate `~/collatz/version.rb`, with the suggestion seeming to be to either do away with having a subfolder that contains `example.rb` files that are then included in the main file via `require_relative "collatz/example"`, or move all the contents of the main file into other `*.rb` files that are then required into the main file. The version file was part of what was auto generated by bundler, and I like having the version on its own in its own file, as it makes setting the version file, and reading the version from it in the workflow, much easier.
86
+ 1. The next piece of feedback, and something I wondered why bundler didn't do from the beginning honestly, is to put _**everything**_ inside a `Collatz` module. I assumed when I first saw that this wasn't done automatically by bundler that the contents of a gem that would then be required into a script would set up the namespace similar to python, and disregarded the fact that it appeared not to do that while setting up the RSpec tests. So moving everything into a top level module to unclobber the "public"`?` namespace.
87
+ 1. Change `unless p != 0` to `if p.zero?`. I vaguely recall trying something along the lines of inline conditionals when first setting that up, and thought a rubocop had complained about it _not_ being an "unless" clause, but it might have been some other part of the inlining of the conditional, so will definitely try this change, as it certainly is much more readable.
88
+ 1. In the `reverse_function` rather than do a `pre_values +=`, use the array's `.push` method. Although another user suggest using `<<` instead? It was finally suggested to instead just settle for a multi line if, which isn't _not_ a reasonable suggestion, even if my preference happens to be for flatter lines. It was also made apparent that similar to the `.zero?` function, there is a `.nonzero?` function that can be used instead of the negated assertion of `.zero?`.
89
+ 1. Instead of setting methods to private with `private :stopping_time_terminus` following the method declaration, simply add `private` before the def (e.g. `private def stopping_time_terminus`). A few sites indicated the same concern another user noted that doing so would make all the methods declared after it also private, however this concern was dissuaded with the assurance that it would not behave that way, instead asserting that the `def something` would return a `Symbol`, which would make `private def something` the same as subsequently passing the symbol of the method to the private modifier.
90
+ 1. The abuse of `NotImplementedError`, which is meant to be ruby internal only, was mentioned. I was aware of this ahead of time, and probably could have done without adding the function headers before implementing them.
91
+ 1. On the `KNOWN_CYCLES`, rather than apply the `.freeze` to all elements in the list individually, set `.each(&:freeze).freeze` on the final outer closing bracket `]` of the whole list.
92
+
93
+ I've fixed most of the above, although some aren't operable at the moment. Setting methods private via `private def method_name` instead of `private :method_name` after the method declaration is causing rubocop to break. I'll get an error message `An error occurred while Style/AccessModifierDeclarations cop was inspecting /mnt/c/Workspaces/GitHub_Skenvy/Collatz/ruby/lib/collatz.rb:71:0.` for both lines, which is then followed by this stack trace.
94
+ ```
95
+ uninitialized constant RuboCop::Version::Server
96
+ Did you mean? TCPServer
97
+ /var/lib/gems/2.7.0/gems/rubocop-1.36.0/lib/rubocop/version.rb:22:in `version'
98
+ /var/lib/gems/2.7.0/gems/rubocop-1.36.0/lib/rubocop/cli/command/execute_runner.rb:82:in `display_error_summary'
99
+ /var/lib/gems/2.7.0/gems/rubocop-1.36.0/lib/rubocop/cli/command/execute_runner.rb:58:in `display_summary'
100
+ /var/lib/gems/2.7.0/gems/rubocop-1.36.0/lib/rubocop/cli/command/execute_runner.rb:27:in `block in execute_runner'
101
+ /var/lib/gems/2.7.0/gems/rubocop-1.36.0/lib/rubocop/cli/command/execute_runner.rb:52:in `with_redirect'
102
+ /var/lib/gems/2.7.0/gems/rubocop-1.36.0/lib/rubocop/cli/command/execute_runner.rb:25:in `execute_runner'
103
+ /var/lib/gems/2.7.0/gems/rubocop-1.36.0/lib/rubocop/cli/command/execute_runner.rb:17:in `run'
104
+ /var/lib/gems/2.7.0/gems/rubocop-1.36.0/lib/rubocop/cli/command.rb:11:in `run'
105
+ /var/lib/gems/2.7.0/gems/rubocop-1.36.0/lib/rubocop/cli/environment.rb:18:in `run'
106
+ /var/lib/gems/2.7.0/gems/rubocop-1.36.0/lib/rubocop/cli.rb:72:in `run_command'
107
+ /var/lib/gems/2.7.0/gems/rubocop-1.36.0/lib/rubocop/cli.rb:79:in `execute_runners'
108
+ /var/lib/gems/2.7.0/gems/rubocop-1.36.0/lib/rubocop/cli.rb:48:in `run'
109
+ /var/lib/gems/2.7.0/gems/rubocop-1.36.0/lib/rubocop/rake_task.rb:51:in `run_cli'
110
+ /var/lib/gems/2.7.0/gems/rubocop-1.36.0/lib/rubocop/rake_task.rb:26:in `block (2 levels) in initialize'
111
+ /var/lib/gems/2.7.0/gems/rake-13.0.6/lib/rake/file_utils_ext.rb:58:in `verbose'
112
+ /var/lib/gems/2.7.0/gems/rubocop-1.36.0/lib/rubocop/rake_task.rb:24:in `block in initialize'
113
+ /var/lib/gems/2.7.0/gems/rake-13.0.6/lib/rake/task.rb:281:in `block in execute'
114
+ /var/lib/gems/2.7.0/gems/rake-13.0.6/lib/rake/task.rb:281:in `each'
115
+ /var/lib/gems/2.7.0/gems/rake-13.0.6/lib/rake/task.rb:281:in `execute'
116
+ /var/lib/gems/2.7.0/gems/rake-13.0.6/lib/rake/task.rb:219:in `block in invoke_with_call_chain'
117
+ /var/lib/gems/2.7.0/gems/rake-13.0.6/lib/rake/task.rb:199:in `synchronize'
118
+ /var/lib/gems/2.7.0/gems/rake-13.0.6/lib/rake/task.rb:199:in `invoke_with_call_chain'
119
+ /var/lib/gems/2.7.0/gems/rake-13.0.6/lib/rake/task.rb:188:in `invoke'
120
+ /var/lib/gems/2.7.0/gems/rake-13.0.6/lib/rake/application.rb:160:in `invoke_task'
121
+ /var/lib/gems/2.7.0/gems/rake-13.0.6/lib/rake/application.rb:116:in `block (2 levels) in top_level'
122
+ /var/lib/gems/2.7.0/gems/rake-13.0.6/lib/rake/application.rb:116:in `each'
123
+ /var/lib/gems/2.7.0/gems/rake-13.0.6/lib/rake/application.rb:116:in `block in top_level'
124
+ /var/lib/gems/2.7.0/gems/rake-13.0.6/lib/rake/application.rb:125:in `run_with_threads'
125
+ /var/lib/gems/2.7.0/gems/rake-13.0.6/lib/rake/application.rb:110:in `top_level'
126
+ /var/lib/gems/2.7.0/gems/rake-13.0.6/lib/rake/application.rb:83:in `block in run'
127
+ /var/lib/gems/2.7.0/gems/rake-13.0.6/lib/rake/application.rb:186:in `standard_exception_handling'
128
+ /var/lib/gems/2.7.0/gems/rake-13.0.6/lib/rake/application.rb:80:in `run'
129
+ /var/lib/gems/2.7.0/gems/rake-13.0.6/exe/rake:27:in `<top (required)>'
130
+ /usr/local/bin/rake:23:in `load'
131
+ /usr/local/bin/rake:23:in `<main>'
132
+ RuboCop failed!
133
+ ```
134
+ It appears though that this error may have already had a bug raised and fixed at [rubocop/issues/10994](https://github.com/rubocop/rubocop/issues/10994). It appears in the meantime however that the `Style/AccessModifierDeclarations` cop isn't working as intended. Setting the below does not cause it to error on the `private :method_name` after the method declaration, which is what `AllowModifiersOnSymbols: false` is supposed to disallow.
135
+ ```yaml
136
+ Style/AccessModifierDeclarations:
137
+ EnforcedStyle: inline
138
+ AllowModifiersOnSymbols: false
139
+ ```
140
+ Even though it is not causing it to throw a cop error on the modifier on symbol pattern `private :method_name`, having the above config _is_ preventing it from throwing the hard error above `uninitialized constant RuboCop::Version::Server` that stopped the whole rubocop process, and indeed it passes with `no offenses detected`.
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Collatz
4
+ # The four known cycles for the standard parameterisation.
5
+ KNOWN_CYCLES = [
6
+ [1, 4, 2], [-1, -2], [-5, -14, -7, -20, -10],
7
+ [-17, -50, -25, -74, -37, -110, -55, -164, -82, -41, -122, -61, -182, -91, -272, -136, -68, -34]
8
+ ].each(&:freeze).freeze
9
+ # The current value up to which the standard parameterisation has been verified.
10
+ VERIFIED_MAXIMUM = 295147905179352825856
11
+ # The current value down to which the standard parameterisation has been verified.
12
+ VERIFIED_MINIMUM = -272 # TODO: Check the actual lowest bound.
13
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "utilities"
4
+
5
+ module Collatz # rubocop:disable Style/Documentation
6
+ module_function # rubocop:disable Style/AccessModifierDeclarations
7
+
8
+ # Handles the sanity check for the parameterisation (p,a,b)
9
+ # required by both the function and reverse function.
10
+ #
11
+ # @raise FailedSaneParameterCheck If p or a are 0.
12
+ #
13
+ # @param +Integer+ p Modulus used to devide n, iff n is equivalent to (0 mod p)
14
+ #
15
+ # @param +Integer+ a Factor by which to multiply n.
16
+ #
17
+ # @param +Integer+ b Value to add to the scaled value of n.
18
+ private def assert_sane_parameterisation(p, a, _b) # :nodoc:
19
+ # Sanity check (p,a,b) ~ p absolutely can't be 0. a "could" be zero
20
+ # theoretically, although would violate the reversability (if ~a is 0 then a
21
+ # value of "b" as the input to the reverse function would have a pre-emptive
22
+ # value of every number not divisible by p). The function doesn't _have_ to
23
+ # be reversable, but we are only interested in dealing with the class of
24
+ # functions that exhibit behaviour consistant with the collatz function. If
25
+ # _every_ input not divisable by p went straight to "b", it would simply
26
+ # cause a cycle consisting of "b" and every b/p^z that is an integer. While
27
+ # p in [-1, 1] could also be a reasonable check, as it makes every value
28
+ # either a 1 or 2 length cycle, it's not strictly an illegal operation.
29
+ # "b" being zero would cause behaviour not consistant with the collatz
30
+ # function, but would not violate the reversability, so no check either.
31
+ # " != 0" is redundant for python assertions.
32
+ raise FailedSaneParameterCheck, SaneParameterErrMsg::SANE_PARAMS_P if p.zero?
33
+ raise FailedSaneParameterCheck, SaneParameterErrMsg::SANE_PARAMS_A if a.zero?
34
+ end
35
+
36
+ # Returns the output of a single application of a Collatz-esque function.
37
+ #
38
+ # @raise FailedSaneParameterCheck If p or a are 0.
39
+ #
40
+ # @param +Integer+ *n:* The value on which to perform the Collatz-esque function.
41
+ #
42
+ # @param +Integer+ *p:* Modulus used to devide n, iff n is equivalent to (0 mod p).
43
+ #
44
+ # @param +Integer+ *a:* Factor by which to multiply n.
45
+ #
46
+ # @param +Integer+ *b:* Value to add to the scaled value of n.
47
+ #
48
+ # @return +Integer+ The result of the function
49
+ def function(n, p: 2, a: 3, b: 1)
50
+ assert_sane_parameterisation(p, a, b)
51
+ (n%p).zero? ? (n/p) : ((a*n)+b)
52
+ end
53
+
54
+ # Returns the output of a single application of a Collatz-esque reverse function. If
55
+ # only one value is returned, it is the value that would be divided by p. If two values
56
+ # are returned, the first is the value that would be divided by p, and the second value
57
+ # is that which would undergo the multiply and add step, regardless of which is larger.
58
+ #
59
+ # @raise FailedSaneParameterCheck If p or a are 0.
60
+ #
61
+ # @param +Integer+ *n:* The value on which to perform the reverse Collatz function.
62
+ #
63
+ # @param +Integer+ *p:* Modulus used to devide n, iff n is equivalent to (0 mod p).
64
+ #
65
+ # @param +Integer+ *a:* Factor by which to multiply n.
66
+ #
67
+ # @param +Integer+ *b:* Value to add to the scaled value of n.
68
+ #
69
+ # @return +List<Integer>+ The values that would return the input if given to the function.
70
+ def reverse_function(n, p: 2, a: 3, b: 1)
71
+ assert_sane_parameterisation(p, a, b)
72
+ if ((n-b)%a).zero? && ((n-b)%(p*a)).nonzero?
73
+ [p*n, (n-b)/a]
74
+ else
75
+ [p*n]
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "utilities"
4
+ require_relative "function"
5
+
6
+ module Collatz # rubocop:disable Style/Documentation
7
+ # Contains the results of computing a hailstone sequence via hailstone_sequence(~).
8
+ class HailstoneSequence
9
+ # The set of values that comprise the hailstone sequence.
10
+ attr_reader :values
11
+
12
+ # A terminal condition that reflects the final state of the hailstone sequencing,
13
+ # whether than be that it succeeded at determining the stopping time, the total
14
+ # stopping time, found a cycle, or got stuck on zero (or surpassed the max total).
15
+ attr_reader :terminal_condition # SequenceState
16
+
17
+ # A status value that has different meanings depending on what the terminal condition
18
+ # was. If the sequence completed either via reaching the stopping or total stopping time,
19
+ # or getting stuck on zero, then this value is the stopping/terminal time. If the sequence
20
+ # got stuck on a cycle, then this value is the cycle length. If the sequencing passes the
21
+ # maximum stopping time then this is the value that was provided as that maximum.
22
+ attr_reader :terminal_status
23
+
24
+ # Initialise and compute a new Hailstone Sequence.
25
+ #
26
+ # @raise FailedSaneParameterCheck If p or a are 0.
27
+ #
28
+ # @param +Integer+ initial_value The value to begin the hailstone sequence from.
29
+ #
30
+ # @param +Integer+ *p:* Modulus used to devide n, iff n is equivalent to (0 mod p).
31
+ #
32
+ # @param +Integer+ *a:* Factor by which to multiply n.
33
+ #
34
+ # @param +Integer+ *b:* Value to add to the scaled value of n.
35
+ #
36
+ # @param +Integer+ *max_total_stopping_time:* Maximum amount of times to iterate the function, if 1 is not reached.
37
+ #
38
+ # @param +Boolean+ *total_stopping_time:* Whether or not to execute until the "total" stopping time
39
+ # (number of iterations to obtain 1) rather than the regular stopping time (number
40
+ # of iterations to reach a value less than the initial value).
41
+ #
42
+ # @return HailstoneSequence An initialised, and computed, hailstone sequence
43
+ def initialize(initial_value, p, a, b, max_total_stopping_time, total_stopping_time)
44
+ terminate = stopping_time_terminus(initial_value, total_stopping_time)
45
+ if initial_value.zero?
46
+ # 0 is always an immediate stop.
47
+ @values = [0]
48
+ @terminal_condition = SequenceState::ZERO_STOP
49
+ @terminal_status = 0
50
+ elsif initial_value == 1
51
+ # 1 is always an immediate stop, with 0 stopping time.
52
+ @values = [1]
53
+ @terminal_condition = SequenceState::TOTAL_STOPPING_TIME
54
+ @terminal_status = 0
55
+ else
56
+ # Otherwise, hail!
57
+ min_max_total_stopping_time = [max_total_stopping_time, 1].max
58
+ pre_values = Array.new(min_max_total_stopping_time+1)
59
+ pre_values[0] = initial_value
60
+ for k in 1..min_max_total_stopping_time do
61
+ next_value = Collatz.function(pre_values[k-1], p: p, a: a, b: b)
62
+ # Check if the next_value hailstone is either the stopping time, total
63
+ # stopping time, the same as the initial value, or stuck at zero.
64
+ if terminate.call(next_value)
65
+ pre_values[k] = next_value
66
+ if next_value == 1
67
+ @terminal_condition = SequenceState::TOTAL_STOPPING_TIME
68
+ else
69
+ @terminal_condition = SequenceState::STOPPING_TIME
70
+ end
71
+ @terminal_status = k
72
+ @values = pre_values.slice(0, k+1)
73
+ return
74
+ end
75
+ if pre_values.include? next_value
76
+ pre_values[k] = next_value
77
+ cycle_init = 1
78
+ for j in 1..k do
79
+ if pre_values[k-j] == next_value
80
+ cycle_init = j
81
+ break
82
+ end
83
+ end
84
+ @terminal_condition = SequenceState::CYCLE_LENGTH
85
+ @terminal_status = cycle_init
86
+ @values = pre_values.slice(0, k+1)
87
+ return
88
+ end
89
+ if next_value.zero?
90
+ pre_values[k] = 0
91
+ @terminal_condition = SequenceState::ZERO_STOP
92
+ @terminal_status = -k
93
+ @values = pre_values.slice(0, k+1)
94
+ return
95
+ end
96
+ pre_values[k] = next_value
97
+ end
98
+ @terminal_condition = SequenceState::MAX_STOP_OUT_OF_BOUNDS
99
+ @terminal_status = min_max_total_stopping_time
100
+ @values = pre_values
101
+ end
102
+ end
103
+
104
+ # Provides the appropriate lambda to use to check if iterations on an initial
105
+ # value have reached either the stopping time, or total stopping time.
106
+ #
107
+ # @param +Integer+ *n:* The initial value to confirm against a stopping time check.
108
+ #
109
+ # @param +Boolean+ *total_stop:* If false, the lambda will confirm that iterations of n
110
+ # have reached the oriented stopping time to reach a value closer to 0.
111
+ # If true, the lambda will simply check equality to 1.
112
+ #
113
+ # @return +lambda<Integer>:<Boolean>+ The lambda to check for the stopping time.
114
+ private def stopping_time_terminus(n, total_stop)
115
+ if total_stop
116
+ lambda { |x| x == 1 }
117
+ elsif n >= 0
118
+ lambda { |x| (x < n && x.positive?) }
119
+ else
120
+ lambda { |x| (x > n && x.negative?) }
121
+ end
122
+ end
123
+ end
124
+
125
+ # Returns a list of successive values obtained by iterating a Collatz-esque
126
+ # function, until either 1 is reached, or the total amount of iterations
127
+ # exceeds max_total_stopping_time, unless total_stopping_time is False,
128
+ # which will terminate the hailstone at the "stopping time" value, i.e. the
129
+ # first value less than the initial value. While the sequence has the
130
+ # capability to determine that it has encountered a cycle, the cycle from "1"
131
+ # wont be attempted or reported as part of a cycle, regardless of default or
132
+ # custom parameterisation, as "1" is considered a "total stop".
133
+ #
134
+ # @raise FailedSaneParameterCheck If p or a are 0.
135
+ #
136
+ # @param +Integer+ *initial_value:* The value to begin the hailstone sequence from.
137
+ #
138
+ # @param +Integer+ *p:* Modulus used to devide n, iff n is equivalent to (0 mod p).
139
+ #
140
+ # @param +Integer+ *a:* Factor by which to multiply n.
141
+ #
142
+ # @param +Integer+ *b:* Value to add to the scaled value of n.
143
+ #
144
+ # @param +Integer+ *max_total_stopping_time:* Maximum amount of times to iterate the function, if 1 is not reached.
145
+ #
146
+ # @param +Boolean+ *total_stopping_time:* Whether or not to execute until the "total" stopping time
147
+ # (number of iterations to obtain 1) rather than the regular stopping time (number
148
+ # of iterations to reach a value less than the initial value).
149
+ #
150
+ # @return HailstoneSequence A set of values that form the hailstone sequence.
151
+ module_function def hailstone_sequence(initial_value, p: 2, a: 3, b: 1, max_total_stopping_time: 1000, total_stopping_time: true) # rubocop:disable Layout/LineLength
152
+ # Prior to starting the hailstone, which has some magic case handling,
153
+ # call the function to trigger any assert_sane_parameterisation flags.
154
+ _throwaway = function(initial_value, p: p, a: a, b: b)
155
+ # Return the hailstone sequence.
156
+ HailstoneSequence.new(initial_value, p, a, b, max_total_stopping_time, total_stopping_time)
157
+ end
158
+
159
+ # Returns the stopping time, the amount of iterations required to reach a
160
+ # value less than the initial value, or nil if max_stopping_time is exceeded.
161
+ # Alternatively, if total_stopping_time is true, then it will instead count
162
+ # the amount of iterations to reach 1. If the sequence does not stop, but
163
+ # instead ends in a cycle, the result will be infinity.
164
+ # If (p,a,b) are such that it is possible to get stuck on zero, the result
165
+ # will be the negative of what would otherwise be the "total stopping time"
166
+ # to reach 1, where 0 is considered a "total stop" that should not occur as
167
+ # it does form a cycle of length 1.
168
+ #
169
+ # @raise FailedSaneParameterCheck If p or a are 0.
170
+ #
171
+ # @param +Integer+ *initial_value:* The value for which to find the stopping time.
172
+ #
173
+ # @param +Integer+ *p:* Modulus used to devide n, iff n is equivalent to (0 mod p).
174
+ #
175
+ # @param +Integer+ *a:* Factor by which to multiply n.
176
+ #
177
+ # @param +Integer+ *b:* Value to add to the scaled value of n.
178
+ #
179
+ # @param +Integer+ *max_stopping_time:* Maximum amount of times to iterate the function, if
180
+ # the stopping time is not reached. IF the max_stopping_time is reached,
181
+ # the function will return nil.
182
+ #
183
+ # @param +Boolean+ *total_stopping_time:* Whether or not to execute until the "total" stopping
184
+ # time (number of iterations to obtain 1) rather than the regular stopping
185
+ # time (number of iterations to reach a value less than the initial value).
186
+ #
187
+ # @return +Integer+ The stopping time, or, in a special case, infinity, nil or a negative.
188
+ module_function def stopping_time(initial_value, p: 2, a: 3, b: 1, max_stopping_time: 1000, total_stopping_time: false) # rubocop:disable Layout/LineLength
189
+ # Although the "max_~_time" for hailstones is named for "total stopping" time
190
+ # and the "max_~_time" for this "stopping time" function is _not_ "total",
191
+ # they are handled the same way, as the default for "total_stopping_time"
192
+ # for hailstones is true, but for this, is false. Thus the naming difference.
193
+ # rubocop:disable Layout/HashAlignment
194
+ hail = hailstone_sequence(initial_value, p: p, a: a, b: b,
195
+ max_total_stopping_time: max_stopping_time,
196
+ total_stopping_time: total_stopping_time)
197
+ # rubocop:enable Layout/HashAlignment
198
+ # For total/regular/zero stopping time, the value is already the same as
199
+ # that present, for cycles we report infinity instead of the cycle length,
200
+ # and for max stop out of bounds, we report None instead of the max stop cap
201
+ { SequenceState::TOTAL_STOPPING_TIME => hail.terminal_status,
202
+ SequenceState::STOPPING_TIME => hail.terminal_status,
203
+ SequenceState::CYCLE_LENGTH => Float::INFINITY,
204
+ SequenceState::ZERO_STOP => hail.terminal_status,
205
+ SequenceState::MAX_STOP_OUT_OF_BOUNDS => nil
206
+ }.fetch(hail.terminal_condition, nil)
207
+ end
208
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "utilities"
4
+ require_relative "function"
5
+
6
+ module Collatz # rubocop:disable Style/Documentation
7
+ # Nodes that form a "tree graph", structured as a tree, with their own node's value,
8
+ # as well as references to either possible child node, where a node can only ever have
9
+ # two children, as there are only ever two reverse values. Also records any possible
10
+ # "terminal sequence state", whether that be that the "orbit distance" has been reached,
11
+ # as an "out of bounds" stop, which is the regularly expected terminal state. Other
12
+ # terminal states possible however include the cycle state and cycle length (end) states.
13
+ class TreeGraphNode
14
+ # The value of this node in the tree.
15
+ attr_reader :node_value
16
+
17
+ # The terminal SequenceState; nil if not a terminal node, MAX_STOP_OUT_OF_BOUNDS if the max_orbit_distance
18
+ # has been reached, CYCLE_LENGTH if the node's value is found to have occured previously, or
19
+ # CYCLE_INIT, retroactively applied when a CYCLE_LENGTH state node is found.
20
+ attr_accessor :terminal_sequence_state
21
+
22
+ # The "Pre N/P" TreeGraphNode child of this node that
23
+ # is always present if this is not a terminal node.
24
+ attr_reader :pre_n_div_p_node
25
+
26
+ # The "Pre aN+b" TreeGraphNode child of this node that is
27
+ # present if it exists and this is not a terminal node.
28
+ attr_reader :pre_an_plus_b_node
29
+
30
+ # Create an instance of TreeGraphNode which will yield its entire sub-tree of all child nodes.
31
+ #
32
+ # @raise FailedSaneParameterCheck If p or a are 0.
33
+ #
34
+ # @param +Integer+ *node_value:* The value for which to find the tree graph node reversal.
35
+ #
36
+ # @param +Integer+ *max_orbit_distance:* The maximum distance/orbit/branch length to travel.
37
+ #
38
+ # @param +Integer+ *p:* Modulus used to devide n, iff n is equivalent to (0 mod p).
39
+ #
40
+ # @param +Integer+ *a:* Factor by which to multiply n.
41
+ #
42
+ # @param +Integer+ *b:* Value to add to the scaled value of n.
43
+ #
44
+ # @param +Hash<Integer>+ *cycle_check:* A hash used to keep track of already graphed values
45
+ #
46
+ # @param +Boolean+ *create_raw:* Used to instruct the initialiser method to take 1:1 inputs, used in testing.
47
+ #
48
+ # @param SequenceState *terminal_sequence_state:*
49
+ #
50
+ # @param TreeGraphNode *pre_n_div_p_node:*
51
+ #
52
+ # @param TreeGraphNode *pre_an_plus_b_node:*
53
+ #
54
+ # @return [TreeGraphNode] A computed tree graph node
55
+ def initialize(node_value, max_orbit_distance, p, a, b, cycle_check: nil, create_raw: false,
56
+ terminal_sequence_state: nil, pre_n_div_p_node: nil, pre_an_plus_b_node: nil)
57
+ @node_value = node_value
58
+ if create_raw
59
+ @terminal_sequence_state = terminal_sequence_state
60
+ @pre_n_div_p_node = pre_n_div_p_node
61
+ @pre_an_plus_b_node = pre_an_plus_b_node
62
+ return
63
+ end
64
+ # Handle cycle prevention for recursive calls
65
+ if cycle_check.nil?
66
+ cycle_check = { @node_value => self }
67
+ elsif !cycle_check[@node_value].nil?
68
+ # The value already exists in the cycle so this is a cyclic terminal
69
+ cycle_check[@node_value].terminal_sequence_state = SequenceState::CYCLE_INIT
70
+ @terminal_sequence_state = SequenceState::CYCLE_LENGTH
71
+ @pre_n_div_p_node = nil
72
+ @pre_an_plus_b_node = nil
73
+ return
74
+ else
75
+ cycle_check[@node_value] = self
76
+ end
77
+ if [0, max_orbit_distance].max.zero?
78
+ @terminal_sequence_state = SequenceState::MAX_STOP_OUT_OF_BOUNDS
79
+ @pre_n_div_p_node = nil
80
+ @pre_an_plus_b_node = nil
81
+ else
82
+ reverses = Collatz.reverse_function(node_value, p: p, a: a, b: b)
83
+ @pre_n_div_p_node = TreeGraphNode.new(reverses[0], max_orbit_distance-1, p, a, b, cycle_check: cycle_check)
84
+ if reverses.length == 2
85
+ @pre_an_plus_b_node = TreeGraphNode.new(reverses[1], max_orbit_distance-1, p, a, b, cycle_check: cycle_check)
86
+ else
87
+ @pre_an_plus_b_node = nil
88
+ end
89
+ end
90
+ end
91
+
92
+ # This will only confirm an equality if the whole subtree of both nodes, including
93
+ # node values, sequence states, and child nodes, checked recursively, are equal.
94
+ #
95
+ # @param TreeGraphNode *tgn:* The TreeGraphNode with which to compare equality.
96
+ #
97
+ # @return +Boolean+ true, if the entire sub-trees are equal.
98
+ def sub_tree_equals(tgn)
99
+ return false if self.node_value != tgn.node_value
100
+ return false if self.terminal_sequence_state != tgn.terminal_sequence_state
101
+ return false if self.pre_n_div_p_node.nil? && !tgn.pre_n_div_p_node.nil?
102
+ return false if !self.pre_n_div_p_node.nil? && !self.pre_n_div_p_node.sub_tree_equals(tgn.pre_n_div_p_node)
103
+ return false if self.pre_an_plus_b_node.nil? && !tgn.pre_an_plus_b_node.nil?
104
+ return false if !self.pre_an_plus_b_node.nil? && !self.pre_an_plus_b_node.sub_tree_equals(tgn.pre_an_plus_b_node)
105
+ true
106
+ end
107
+ end
108
+
109
+ # Contains the results of computing the Tree Graph via tree_graph(~).
110
+ # Contains the root node of a tree of TreeGraphNode's.
111
+ class TreeGraph
112
+ # The root node of the tree of TreeGraphNode's.
113
+ attr_reader :root
114
+
115
+ # Create a new TreeGraph with the root node defined by the inputs.
116
+ #
117
+ # @raise FailedSaneParameterCheck If p or a are 0.
118
+ #
119
+ # @param +Integer+ *node_value:* The value for which to find the tree graph node reversal.
120
+ #
121
+ # @param +Integer+ *max_orbit_distance:* The maximum distance/orbit/branch length to travel.
122
+ #
123
+ # @param +Integer+ *p:* Modulus used to devide n, iff n is equivalent to (0 mod p).
124
+ #
125
+ # @param +Integer+ *a:* Factor by which to multiply n.
126
+ #
127
+ # @param +Integer+ *b:* Value to add to the scaled value of n.
128
+ #
129
+ # @param +Boolean+ *create_raw:* Used to instruct the initialiser method to take 1:1 inputs, used in testing.
130
+ #
131
+ # @param TreeGraphNode *root:* A node that will be set to the root of this tree
132
+ #
133
+ # @return [TreeGraph] A tree graph, with a computed root node.
134
+ def initialize(node_value, max_orbit_distance, p, a, b, create_raw: false, root: nil)
135
+ if create_raw && !root.nil?
136
+ @root = root
137
+ else
138
+ @root = TreeGraphNode.new(node_value, max_orbit_distance, p, a, b)
139
+ end
140
+ end
141
+
142
+ # The equality between TreeGraph's is determined by the equality check on
143
+ # subtrees. A subtree check will be done on both TreeGraph's root nodes.
144
+ #
145
+ # @param TreeGraph
146
+ #
147
+ # @return +Boolean+ true, if both are TreeGraph, with the entire root's sub-trees being equal.
148
+ def ==(other)
149
+ # Generic checks
150
+ return false if other.nil?
151
+ return false unless other.is_a?(self.class)
152
+ # Actual check
153
+ self.root.sub_tree_equals(other.root)
154
+ end
155
+ end
156
+
157
+ # Returns a directed tree graph of the reverse function values up to a maximum
158
+ # nesting of max_orbit_distance, with the initial_value as the root.
159
+ #
160
+ # @raise FailedSaneParameterCheck If p or a are 0.
161
+ #
162
+ # @param +Integer+ *initial_value:* The root value of the directed tree graph.
163
+ #
164
+ # @param +Integer+ *max_orbit_distance:* Maximum amount of times to iterate the reverse
165
+ # function. There is no natural termination to populating the tree graph, equivalent
166
+ # to the termination of hailstone sequences or stopping time attempts, so this is not
167
+ # an optional argument like max_stopping_time / max_total_stopping_time, as it is the
168
+ # intended target of orbits to obtain, rather than a limit to avoid uncapped computation.
169
+ #
170
+ # @param +Integer+ *p:* Modulus used to devide n, iff n is equivalent to (0 mod p).
171
+ #
172
+ # @param +Integer+ *a:* Factor by which to multiply n.
173
+ #
174
+ # @param +Integer+ *b:* Value to add to the scaled value of n.
175
+ #
176
+ # @return TreeGraph The branches of the tree graph as determined by the reverse function.
177
+ module_function def tree_graph(initial_value, max_orbit_distance, p: 2, a: 3, b: 1)
178
+ # Prior to starting the tree graph, which has some magic case handling, call
179
+ # the reverse_function to trigger any assert_sane_parameterisation flags.
180
+ _throwaway = reverse_function(initial_value, p: p, a: a, b: b)
181
+ TreeGraph.new(initial_value, max_orbit_distance, p, a, b)
182
+ end
183
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Collatz
4
+ # Error message constants, to be used as input to the FailedSaneParameterCheck.
5
+ module SaneParameterErrMsg
6
+ # Message to print in the FailedSaneParameterCheck if p, the modulus, is zero.
7
+ SANE_PARAMS_P = "'p' should not be 0 ~ violates modulo being non-zero."
8
+ # Message to print in the FailedSaneParameterCheck if a, the multiplicand, is zero.
9
+ SANE_PARAMS_A = "'a' should not be 0 ~ violates the reversability."
10
+ end
11
+
12
+ # SequenceState for Cycle Control: Descriptive flags to indicate when some event occurs in the
13
+ # hailstone sequences or tree graph reversal, when set to verbose, or stopping time check.
14
+ module SequenceState
15
+ # A Hailstone sequence state that indicates the stopping
16
+ # time, a value less than the initial, has been reached.
17
+ STOPPING_TIME = "STOPPING_TIME"
18
+ # A Hailstone sequence state that indicates the total
19
+ # stopping time, a value of 1, has been reached.
20
+ TOTAL_STOPPING_TIME = "TOTAL_STOPPING_TIME"
21
+ # A Hailstone and TreeGraph sequence state that indicates the
22
+ # first occurence of a value that subsequently forms a cycle.
23
+ CYCLE_INIT = "CYCLE_INIT"
24
+ # A Hailstone and TreeGraph sequence state that indicates the
25
+ # last occurence of a value that has already formed a cycle.
26
+ CYCLE_LENGTH = "CYCLE_LENGTH"
27
+ # A Hailstone and TreeGraph sequence state that indicates the sequence
28
+ # or traversal has executed some imposed 'maximum' amount of times.
29
+ MAX_STOP_OUT_OF_BOUNDS = "MAX_STOP_OUT_OF_BOUNDS"
30
+ # A Hailstone sequence state that indicates the sequence terminated
31
+ # by reaching "0", a special type of "stopping time".
32
+ ZERO_STOP = "ZERO_STOP"
33
+ end
34
+
35
+ # Thrown when either p, the modulus, or a, the multiplicand, are zero.
36
+ class FailedSaneParameterCheck < StandardError
37
+ # Construct a FailedSaneParameterCheck with a message associated with the provided enum.
38
+ # @param [String] msg One of the SaneParameterErrMsg strings.
39
+ def initialize(msg = "This is a custom exception", exception_type = "FailedSaneParameterCheck")
40
+ @exception_type = exception_type
41
+ super(msg)
42
+ end
43
+ end
44
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Collatz
4
- VERSION = "0.0.4"
4
+ # The current semver version.
5
+ VERSION = "1.0.0"
5
6
  end
data/lib/collatz.rb CHANGED
@@ -1,8 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "collatz/version"
4
+ require_relative "collatz/constants"
5
+ require_relative "collatz/utilities"
6
+ require_relative "collatz/function"
7
+ require_relative "collatz/hailstone_sequence"
8
+ require_relative "collatz/tree_graph"
4
9
 
10
+ # Provides the basic functionality to interact with the Collatz conjecture. The
11
+ # parameterisation uses the same (p,a,b) notation as Conway's generalisations.
12
+ # Besides the function and reverse function, there is also functionality to
13
+ # retrieve the hailstone sequence, the "stopping time"/"total stopping time", or
14
+ # tree-graph.
5
15
  module Collatz
6
- class Error < StandardError; end
7
- # Your code goes here...
16
+ # Using a module to proctor a namespace for the functions, none of which
17
+ # are instance methods. All are "class" methods, so set module_function
18
+ # at the head of each required file which opens this module and has defs.
19
+ # https://github.com/rubocop/ruby-style-guide#modules-vs-classes
20
+ # rubocop:disable Lint/UselessAccessModifier
21
+
22
+ module_function # rubocop:disable Style/AccessModifierDeclarations
23
+
24
+ # rubocop:enable Lint/UselessAccessModifier
8
25
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: collatz
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Levett
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-09-14 00:00:00.000000000 Z
11
+ date: 2022-09-24 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |-
14
14
  Provides the basic functionality to interact with the Collatz conjecture.
@@ -34,15 +34,19 @@ files:
34
34
  - collatz.gemspec
35
35
  - devlog.md
36
36
  - lib/collatz.rb
37
+ - lib/collatz/constants.rb
38
+ - lib/collatz/function.rb
39
+ - lib/collatz/hailstone_sequence.rb
40
+ - lib/collatz/tree_graph.rb
41
+ - lib/collatz/utilities.rb
37
42
  - lib/collatz/version.rb
38
- - sig/collatz.rbs
39
- homepage: https://github.com/Skenvy/Collatz/tree/main/ruby
43
+ homepage: https://skenvy.github.io/Collatz/ruby/
40
44
  licenses:
41
45
  - Apache-2.0
42
46
  metadata:
43
- homepage_uri: https://github.com/Skenvy/Collatz/tree/main/ruby
47
+ homepage_uri: https://skenvy.github.io/Collatz/ruby/
44
48
  source_code_uri: https://github.com/Skenvy/Collatz/tree/main/ruby
45
- post_install_message:
49
+ post_install_message:
46
50
  rdoc_options: []
47
51
  require_paths:
48
52
  - lib
@@ -58,7 +62,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
58
62
  version: '0'
59
63
  requirements: []
60
64
  rubygems_version: 3.1.6
61
- signing_key:
65
+ signing_key:
62
66
  specification_version: 4
63
67
  summary: Functions Related to the Collatz/Syracuse/3n+1 Problem
64
68
  test_files: []
data/sig/collatz.rbs DELETED
@@ -1,4 +0,0 @@
1
- module Collatz
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end