collatz 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +27 -0
- data/Gemfile.lock +2 -2
- data/Rakefile +1 -1
- data/devlog.md +58 -0
- data/lib/collatz/constants.rb +13 -0
- data/lib/collatz/function.rb +78 -0
- data/lib/collatz/hailstone_sequence.rb +208 -0
- data/lib/collatz/tree_graph.rb +183 -0
- data/lib/collatz/utilities.rb +44 -0
- data/lib/collatz/version.rb +2 -1
- data/lib/collatz.rb +13 -225
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 30d4085b4e190a4e1aaf4fdd5f4108abb26688c8223bc1725091d6976b257252
|
4
|
+
data.tar.gz: 3467c2848b02d28909455f092759a8cad3e67eba18583c3873e2bf74c456b11c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9e6d2e859b8cc330406d54989b4a01ee1e83d7e7da1ffad2d66b78eb588aa19e8bcd7b504bd4aeb8320710ae69eb1e2f9d6762a33a5b9b8532a2a91e0190e5f4
|
7
|
+
data.tar.gz: df4b8fa5f25bbc86664ea45d012db4deb07c1197c8addb5fb2f8cb3c22354a89e1612a3b0eb362c8c3bacf2c9b8792a4ba7f746fb40d43ba30066b4aebcd20d8
|
data/.rubocop.yml
CHANGED
@@ -20,6 +20,8 @@ Security/CompoundHash:
|
|
20
20
|
Security/IoMethods:
|
21
21
|
Enabled: true
|
22
22
|
|
23
|
+
Layout/EmptyLineAfterGuardClause:
|
24
|
+
Enabled: false
|
23
25
|
Layout/LineLength:
|
24
26
|
Max: 120
|
25
27
|
Layout/LineContinuationLeadingSpace:
|
@@ -28,15 +30,27 @@ Layout/LineContinuationSpacing:
|
|
28
30
|
Enabled: true
|
29
31
|
Layout/LineEndStringConcatenationIndentation:
|
30
32
|
Enabled: true
|
33
|
+
Layout/MultilineHashBraceLayout:
|
34
|
+
Enabled: false
|
31
35
|
Layout/SpaceAroundOperators:
|
32
36
|
Enabled: false
|
33
37
|
Layout/SpaceBeforeBrackets:
|
34
38
|
Enabled: true
|
35
39
|
|
40
|
+
Metrics/AbcSize:
|
41
|
+
Enabled: false
|
36
42
|
Metrics/BlockLength:
|
37
43
|
AllowedMethods: ['describe', 'context']
|
44
|
+
Metrics/BlockNesting:
|
45
|
+
Enabled: false
|
46
|
+
Metrics/CyclomaticComplexity:
|
47
|
+
Enabled: false
|
48
|
+
Metrics/MethodLength:
|
49
|
+
Enabled: false
|
38
50
|
Metrics/ParameterLists:
|
39
51
|
Enabled: false
|
52
|
+
Metrics/PerceivedComplexity:
|
53
|
+
Enabled: false
|
40
54
|
|
41
55
|
Lint/AmbiguousAssignment:
|
42
56
|
Enabled: true
|
@@ -100,10 +114,15 @@ Style/StringLiterals:
|
|
100
114
|
Style/StringLiteralsInInterpolation:
|
101
115
|
Enabled: true
|
102
116
|
EnforcedStyle: double_quotes
|
117
|
+
Style/AccessModifierDeclarations:
|
118
|
+
EnforcedStyle: inline
|
119
|
+
AllowModifiersOnSymbols: false
|
103
120
|
Style/ArgumentsForwarding:
|
104
121
|
Enabled: true
|
105
122
|
Style/CollectionCompact:
|
106
123
|
Enabled: true
|
124
|
+
Style/ConditionalAssignment:
|
125
|
+
Enabled: false
|
107
126
|
Style/DocumentDynamicEvalDefinition:
|
108
127
|
Enabled: true
|
109
128
|
Style/EmptyHeredoc:
|
@@ -118,6 +137,8 @@ Style/FileRead:
|
|
118
137
|
Enabled: true
|
119
138
|
Style/FileWrite:
|
120
139
|
Enabled: true
|
140
|
+
Style/For:
|
141
|
+
Enabled: false
|
121
142
|
Style/HashConversion:
|
122
143
|
Enabled: true
|
123
144
|
Style/HashExcept:
|
@@ -126,6 +147,8 @@ Style/IfWithBooleanLiteralBranches:
|
|
126
147
|
Enabled: true
|
127
148
|
Style/InPatternThen:
|
128
149
|
Enabled: true
|
150
|
+
Style/Lambda:
|
151
|
+
EnforcedStyle: lambda
|
129
152
|
Style/MagicCommentFormat:
|
130
153
|
Enabled: true
|
131
154
|
Style/MapCompactWithConditionalBlock:
|
@@ -138,6 +161,8 @@ Style/NegatedIfElseCondition:
|
|
138
161
|
Enabled: true
|
139
162
|
Style/NestedFileDirname:
|
140
163
|
Enabled: true
|
164
|
+
Style/Next:
|
165
|
+
Enabled: false
|
141
166
|
Style/NilLambda:
|
142
167
|
Enabled: true
|
143
168
|
Style/NumberedParameters:
|
@@ -156,6 +181,8 @@ Style/RedundantArgument:
|
|
156
181
|
Enabled: true
|
157
182
|
Style/RedundantInitialize:
|
158
183
|
Enabled: true
|
184
|
+
Style/RedundantSelf:
|
185
|
+
Enabled: false
|
159
186
|
Style/RedundantSelfAssignmentBranch:
|
160
187
|
Enabled: true
|
161
188
|
Style/SelectByRegexp:
|
data/Gemfile.lock
CHANGED
data/Rakefile
CHANGED
data/devlog.md
CHANGED
@@ -80,3 +80,61 @@ We're now ready to add once again create an empty orphan branch;
|
|
80
80
|
1. `rm .git/index ; git clean -fdx`
|
81
81
|
1. `git commit -m "Initial empty orphan" --allow-empty`
|
82
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
|
data/lib/collatz/version.rb
CHANGED
data/lib/collatz.rb
CHANGED
@@ -1,237 +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
|
|
5
|
-
# :section: Main
|
6
10
|
# Provides the basic functionality to interact with the Collatz conjecture. The
|
7
11
|
# parameterisation uses the same (p,a,b) notation as Conway's generalisations.
|
8
12
|
# Besides the function and reverse function, there is also functionality to
|
9
13
|
# retrieve the hailstone sequence, the "stopping time"/"total stopping time", or
|
10
14
|
# tree-graph.
|
15
|
+
module Collatz
|
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
|
11
21
|
|
12
|
-
#
|
13
|
-
KNOWN_CYCLES = [
|
14
|
-
[1, 4, 2].freeze, [-1, -2].freeze, [-5, -14, -7, -20, -10].freeze,
|
15
|
-
[-17, -50, -25, -74, -37, -110, -55, -164, -82, -41, -122, -61, -182, -91, -272, -136, -68, -34].freeze
|
16
|
-
].freeze
|
17
|
-
# The current value up to which the standard parameterisation has been verified.
|
18
|
-
VERIFIED_MAXIMUM = 295147905179352825856
|
19
|
-
# The current value down to which the standard parameterisation has been verified.
|
20
|
-
VERIFIED_MINIMUM = -272 # TODO: Check the actual lowest bound.
|
22
|
+
module_function # rubocop:disable Style/AccessModifierDeclarations
|
21
23
|
|
22
|
-
#
|
23
|
-
module SaneParameterErrMsg
|
24
|
-
# Message to print in the FailedSaneParameterCheck if p, the modulus, is zero.
|
25
|
-
SANE_PARAMS_P = "'p' should not be 0 ~ violates modulo being non-zero."
|
26
|
-
# Message to print in the FailedSaneParameterCheck if a, the multiplicand, is zero.
|
27
|
-
SANE_PARAMS_A = "'a' should not be 0 ~ violates the reversability."
|
28
|
-
end
|
29
|
-
|
30
|
-
# Thrown when either p, the modulus, or a, the multiplicand, are zero.
|
31
|
-
class FailedSaneParameterCheck < StandardError
|
32
|
-
# Construct a FailedSaneParameterCheck with a message associated with the provided enum.
|
33
|
-
# @param [String] msg One of the SaneParameterErrMsg strings.
|
34
|
-
def initialize(msg = "This is a custom exception", exception_type = "FailedSaneParameterCheck")
|
35
|
-
@exception_type = exception_type
|
36
|
-
super(msg)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
# SequenceState for Cycle Control: Descriptive flags to indicate when some event occurs in the
|
41
|
-
# hailstone sequences or tree graph reversal, when set to verbose, or stopping time check.
|
42
|
-
module SequenceState
|
43
|
-
# A Hailstone sequence state that indicates the stopping
|
44
|
-
# time, a value less than the initial, has been reached.
|
45
|
-
STOPPING_TIME = "STOPPING_TIME"
|
46
|
-
# A Hailstone sequence state that indicates the total
|
47
|
-
# stopping time, a value of 1, has been reached.
|
48
|
-
TOTAL_STOPPING_TIME = "TOTAL_STOPPING_TIME"
|
49
|
-
# A Hailstone and TreeGraph sequence state that indicates the
|
50
|
-
# first occurence of a value that subsequently forms a cycle.
|
51
|
-
CYCLE_INIT = "CYCLE_INIT"
|
52
|
-
# A Hailstone and TreeGraph sequence state that indicates the
|
53
|
-
# last occurence of a value that has already formed a cycle.
|
54
|
-
CYCLE_LENGTH = "CYCLE_LENGTH"
|
55
|
-
# A Hailstone and TreeGraph sequence state that indicates the sequence
|
56
|
-
# or traversal has executed some imposed 'maximum' amount of times.
|
57
|
-
MAX_STOP_OUT_OF_BOUNDS = "MAX_STOP_OUT_OF_BOUNDS"
|
58
|
-
# A Hailstone sequence state that indicates the sequence terminated
|
59
|
-
# by reaching "0", a special type of "stopping time".
|
60
|
-
ZERO_STOP = "ZERO_STOP"
|
61
|
-
end
|
62
|
-
|
63
|
-
# Handles the sanity check for the parameterisation (p,a,b)
|
64
|
-
# required by both the function and reverse function.
|
65
|
-
#
|
66
|
-
# @raise [FailedSaneParameterCheck] If p or a are 0.
|
67
|
-
#
|
68
|
-
# @param [Integer] p Modulus used to devide n, iff n is equivalent to (0 mod p)
|
69
|
-
# @param [Integer] a Factor by which to multiply n.
|
70
|
-
# @param [Integer] b Value to add to the scaled value of n.
|
71
|
-
def assert_sane_parameterisation(p, a, _b)
|
72
|
-
# Sanity check (p,a,b) ~ p absolutely can't be 0. a "could" be zero
|
73
|
-
# theoretically, although would violate the reversability (if ~a is 0 then a
|
74
|
-
# value of "b" as the input to the reverse function would have a pre-emptive
|
75
|
-
# value of every number not divisible by p). The function doesn't _have_ to
|
76
|
-
# be reversable, but we are only interested in dealing with the class of
|
77
|
-
# functions that exhibit behaviour consistant with the collatz function. If
|
78
|
-
# _every_ input not divisable by p went straight to "b", it would simply
|
79
|
-
# cause a cycle consisting of "b" and every b/p^z that is an integer. While
|
80
|
-
# p in [-1, 1] could also be a reasonable check, as it makes every value
|
81
|
-
# either a 1 or 2 length cycle, it's not strictly an illegal operation.
|
82
|
-
# "b" being zero would cause behaviour not consistant with the collatz
|
83
|
-
# function, but would not violate the reversability, so no check either.
|
84
|
-
# " != 0" is redundant for python assertions.
|
85
|
-
raise FailedSaneParameterCheck, SaneParameterErrMsg::SANE_PARAMS_P unless p != 0
|
86
|
-
raise FailedSaneParameterCheck, SaneParameterErrMsg::SANE_PARAMS_A unless a != 0
|
87
|
-
end
|
88
|
-
|
89
|
-
private :assert_sane_parameterisation
|
90
|
-
|
91
|
-
# Returns the output of a single application of a Collatz-esque function.
|
92
|
-
#
|
93
|
-
# @raise [FailedSaneParameterCheck] If p or a are 0.
|
94
|
-
#
|
95
|
-
# @param [Integer] n The value on which to perform the Collatz-esque function.
|
96
|
-
# @param [Integer] p Modulus used to devide n, iff n is equivalent to (0 mod p).
|
97
|
-
# @param [Integer] a Factor by which to multiply n.
|
98
|
-
# @param [Integer] b Value to add to the scaled value of n.
|
99
|
-
#
|
100
|
-
# @return [Integer] The result of the function
|
101
|
-
def function(n, p: 2, a: 3, b: 1)
|
102
|
-
assert_sane_parameterisation(p, a, b)
|
103
|
-
(n%p).zero? ? (n/p) : ((a*n)+b)
|
104
|
-
end
|
105
|
-
|
106
|
-
# Returns the output of a single application of a Collatz-esque reverse function. If
|
107
|
-
# only one value is returned, it is the value that would be divided by p. If two values
|
108
|
-
# are returned, the first is the value that would be divided by p, and the second value
|
109
|
-
# is that which would undergo the multiply and add step, regardless of which is larger.
|
110
|
-
#
|
111
|
-
# @raise [FailedSaneParameterCheck] If p or a are 0.
|
112
|
-
#
|
113
|
-
# @param [Integer] n The value on which to perform the reverse Collatz function.
|
114
|
-
# @param [Integer] p Modulus used to devide n, iff n is equivalent to (0 mod p).
|
115
|
-
# @param [Integer] a Factor by which to multiply n.
|
116
|
-
# @param [Integer] b Value to add to the scaled value of n.
|
117
|
-
#
|
118
|
-
# @return [Integer] The values that would return the input if given to the function.
|
119
|
-
def reverse_function(n, p: 2, a: 3, b: 1)
|
120
|
-
assert_sane_parameterisation(p, a, b)
|
121
|
-
pre_values = [p*n]
|
122
|
-
pre_values += [(n-b)/a] if ((n-b)%a).zero? && !((n-b)%(p*a)).zero?
|
123
|
-
pre_values
|
124
|
-
end
|
125
|
-
|
126
|
-
# Provides the appropriate lambda to use to check if iterations on an initial
|
127
|
-
# value have reached either the stopping time, or total stopping time.
|
128
|
-
# @param [Integer] n The initial value to confirm against a stopping time check.
|
129
|
-
# @param [Boolean] total_stop If false, the lambda will confirm that iterations of n
|
130
|
-
# have reached the oriented stopping time to reach a value closer to 0.
|
131
|
-
# If true, the lambda will simply check equality to 1.
|
132
|
-
# @return [Method(Integer)->(Boolean)] The lambda to check for the stopping time.
|
133
|
-
def stopping_time_terminus(n, total_stop)
|
134
|
-
raise NotImplementedError, "Will be implemented at, or before, v1.0.0"
|
135
|
-
end
|
136
|
-
|
137
|
-
private :stopping_time_terminus
|
138
|
-
|
139
|
-
# Contains the results of computing a hailstone sequence via hailstone_sequence(~).
|
140
|
-
class HailstoneSequence
|
141
|
-
def initialize
|
142
|
-
raise NotImplementedError, "Will be implemented at, or before, v1.0.0"
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
# Returns a list of successive values obtained by iterating a Collatz-esque
|
147
|
-
# function, until either 1 is reached, or the total amount of iterations
|
148
|
-
# exceeds max_total_stopping_time, unless total_stopping_time is False,
|
149
|
-
# which will terminate the hailstone at the "stopping time" value, i.e. the
|
150
|
-
# first value less than the initial value. While the sequence has the
|
151
|
-
# capability to determine that it has encountered a cycle, the cycle from "1"
|
152
|
-
# wont be attempted or reported as part of a cycle, regardless of default or
|
153
|
-
# custom parameterisation, as "1" is considered a "total stop".
|
154
|
-
#
|
155
|
-
# @raise [FailedSaneParameterCheck] If p or a are 0.
|
156
|
-
#
|
157
|
-
# @param [Integer] initial_value The value to begin the hailstone sequence from.
|
158
|
-
# @param [Integer] p Modulus used to devide n, iff n is equivalent to (0 mod p).
|
159
|
-
# @param [Integer] a Factor by which to multiply n.
|
160
|
-
# @param [Integer] b Value to add to the scaled value of n.
|
161
|
-
# @param [Integer] max_total_stopping_time Maximum amount of times to iterate the function, if 1 is not reached.
|
162
|
-
# @param [Boolean] total_stopping_time Whether or not to execute until the "total" stopping time
|
163
|
-
# (number of iterations to obtain 1) rather than the regular stopping time (number
|
164
|
-
# of iterations to reach a value less than the initial value).
|
165
|
-
#
|
166
|
-
# @return [HailstoneSequence] A set of values that form the hailstone sequence.
|
167
|
-
def hailstone_sequence(initial_value, p: 2, a: 3, b: 1, max_total_stopping_time: 1000, total_stopping_time: true)
|
168
|
-
raise NotImplementedError, "Will be implemented at, or before, v1.0.0"
|
169
|
-
end
|
170
|
-
|
171
|
-
# Returns the stopping time, the amount of iterations required to reach a
|
172
|
-
# value less than the initial value, or nil if max_stopping_time is exceeded.
|
173
|
-
# Alternatively, if total_stopping_time is True, then it will instead count
|
174
|
-
# the amount of iterations to reach 1. If the sequence does not stop, but
|
175
|
-
# instead ends in a cycle, the result will be infinity.
|
176
|
-
# If (p,a,b) are such that it is possible to get stuck on zero, the result
|
177
|
-
# will be the negative of what would otherwise be the "total stopping time"
|
178
|
-
# to reach 1, where 0 is considered a "total stop" that should not occur as
|
179
|
-
# it does form a cycle of length 1.
|
180
|
-
#
|
181
|
-
# @raise [FailedSaneParameterCheck] If p or a are 0.
|
182
|
-
#
|
183
|
-
# @param [Integer] initial_value The value for which to find the stopping time.
|
184
|
-
# @param [Integer] p Modulus used to devide n, iff n is equivalent to (0 mod p).
|
185
|
-
# @param [Integer] a Factor by which to multiply n.
|
186
|
-
# @param [Integer] b Value to add to the scaled value of n.
|
187
|
-
# @param [Integer] max_stopping_time Maximum amount of times to iterate the function, if
|
188
|
-
# the stopping time is not reached. IF the max_stopping_time is reached,
|
189
|
-
# the function will return nil.
|
190
|
-
# @param [Boolean] total_stopping_time Whether or not to execute until the "total" stopping
|
191
|
-
# time (number of iterations to obtain 1) rather than the regular stopping
|
192
|
-
# time (number of iterations to reach a value less than the initial value).
|
193
|
-
#
|
194
|
-
# @return [Integer] The stopping time, or, in a special case, infinity, nil or a negative.
|
195
|
-
def stopping_time(initial_value, p: 2, a: 3, b: 1, max_stopping_time: 1000, total_stopping_time: false)
|
196
|
-
raise NotImplementedError, "Will be implemented at, or before, v1.0.0"
|
197
|
-
end
|
198
|
-
|
199
|
-
# Nodes that form a "tree graph", structured as a tree, with their own node's value,
|
200
|
-
# as well as references to either possible child node, where a node can only ever have
|
201
|
-
# two children, as there are only ever two reverse values. Also records any possible
|
202
|
-
# "terminal sequence state", whether that be that the "orbit distance" has been reached,
|
203
|
-
# as an "out of bounds" stop, which is the regularly expected terminal state. Other
|
204
|
-
# terminal states possible however include the cycle state and cycle length (end) states.
|
205
|
-
class TreeGraphNode
|
206
|
-
def initialize
|
207
|
-
raise NotImplementedError, "Will be implemented at, or before, v1.0.0"
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
# Contains the results of computing the Tree Graph via tree_graph(~).
|
212
|
-
# Contains the root node of a tree of TreeGraphNode's.
|
213
|
-
class TreeGraph
|
214
|
-
def initialize
|
215
|
-
raise NotImplementedError, "Will be implemented at, or before, v1.0.0"
|
216
|
-
end
|
217
|
-
end
|
218
|
-
|
219
|
-
# Returns a directed tree graph of the reverse function values up to a maximum
|
220
|
-
# nesting of max_orbit_distance, with the initial_value as the root.
|
221
|
-
#
|
222
|
-
# @raise [FailedSaneParameterCheck] If p or a are 0.
|
223
|
-
#
|
224
|
-
# @param [Integer] initial_value The root value of the directed tree graph.
|
225
|
-
# @param [Integer] max_orbit_distance Maximum amount of times to iterate the reverse
|
226
|
-
# function. There is no natural termination to populating the tree graph, equivalent
|
227
|
-
# to the termination of hailstone sequences or stopping time attempts, so this is not
|
228
|
-
# an optional argument like max_stopping_time / max_total_stopping_time, as it is the intended
|
229
|
-
# target of orbits to obtain, rather than a limit to avoid uncapped computation.
|
230
|
-
# @param [Integer] p Modulus used to devide n, iff n is equivalent to (0 mod p).
|
231
|
-
# @param [Integer] a Factor by which to multiply n.
|
232
|
-
# @param [Integer] b Value to add to the scaled value of n.
|
233
|
-
#
|
234
|
-
# @return [TreeGraph] The branches of the tree graph as determined by the reverse function.
|
235
|
-
def tree_graph(initial_value, max_orbit_distance, p: 2, a: 3, b: 1, __cycle_prevention: nil)
|
236
|
-
raise NotImplementedError, "Will be implemented at, or before, v1.0.0"
|
24
|
+
# rubocop:enable Lint/UselessAccessModifier
|
237
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:
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nathan Levett
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-09-
|
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,6 +34,11 @@ 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
43
|
homepage: https://skenvy.github.io/Collatz/ruby/
|
39
44
|
licenses:
|