fraction-tree 1.1.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/fraction_tree/extensions.rb +29 -0
- data/lib/fraction_tree/fraction_tree.rb +256 -0
- data/lib/fraction_tree/node.rb +252 -0
- data/lib/fraction_tree.rb +5 -323
- metadata +34 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6a5099eb066c0e5fae9c4553a44b75ca90b52db8c4711b7d7a07247d116e93ea
|
4
|
+
data.tar.gz: 20624486f4b7987331ce3abc1f724a183212d3d403ab207d71a7bdbcf081c3a7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a9abac107904f305c8a23fd34a4fb838b35ddca05046c7713a1c823c82848b85b914c055b2c1a24b3e2b440cfc04d23ad95fd312df4351d7244cfad0c5850a61
|
7
|
+
data.tar.gz: bfac6032e8f9b958e3f766bb16e7160a5ddb56fe2234e6fc79df6fe4317eb823cdcbb374508d1fbd3172b27334a7921a1c0f0c33b82e07ec767c8fc98c116952
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class String
|
2
|
+
# @return [FractionTree::Node] string Stern-Brocot decoded
|
3
|
+
# @example
|
4
|
+
# "1/0".to_node => (1/0)
|
5
|
+
#
|
6
|
+
def to_node
|
7
|
+
if self.include?(".")
|
8
|
+
number = self.to_d
|
9
|
+
numerator, denominator = number.numerator, number.denominator
|
10
|
+
elsif self.include?("/")
|
11
|
+
(numerator, denominator) = self.split("/").map(&:to_i)
|
12
|
+
else
|
13
|
+
number = self.to_r
|
14
|
+
numerator, denominator = number.numerator, number.denominator
|
15
|
+
end
|
16
|
+
number = denominator.zero? ? Float::INFINITY : Rational(numerator, denominator)
|
17
|
+
FractionTree::Node.new(number)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Numeric
|
22
|
+
# @return [FractionTree::Node] string Stern-Brocot decoded
|
23
|
+
# @example
|
24
|
+
# Float::INFINITY.to_node => (1/0)
|
25
|
+
#
|
26
|
+
def to_node
|
27
|
+
FractionTree.node(self)
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,256 @@
|
|
1
|
+
# @author Jose Hales-Garcia
|
2
|
+
#
|
3
|
+
class FractionTree
|
4
|
+
DEFAULT_TREE_DEPTH = 20
|
5
|
+
|
6
|
+
private_class_method :new
|
7
|
+
|
8
|
+
class << self
|
9
|
+
# @return the left-most node of the range of the tree
|
10
|
+
# @example
|
11
|
+
# FractionTree.left_node => 0/1
|
12
|
+
# @note defaults to Stern-Brocot left-most range, 0
|
13
|
+
#
|
14
|
+
def left_node
|
15
|
+
@left_node || 0/1r
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return the right-most node of the range of the tree
|
19
|
+
# @example
|
20
|
+
# FractionTree.right_node => Infinity
|
21
|
+
# @note defaults to Stern-Brocot right-most range, Infinity
|
22
|
+
#
|
23
|
+
def right_node
|
24
|
+
@right_node || Float::INFINITY
|
25
|
+
end
|
26
|
+
|
27
|
+
# Set the left-most node of the range of the tree
|
28
|
+
# @example
|
29
|
+
# FractionTree.left_node = 1/1r
|
30
|
+
#
|
31
|
+
def left_node=(rhs)
|
32
|
+
@left_node = rhs
|
33
|
+
end
|
34
|
+
|
35
|
+
# Set the right-most node of the range of the tree
|
36
|
+
# @example
|
37
|
+
# FractionTree.right_node = 2/1r
|
38
|
+
#
|
39
|
+
def right_node=(rhs)
|
40
|
+
@right_node = rhs
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return the range of the tree
|
44
|
+
# @example
|
45
|
+
# FractionTree.range => (0/1..Infinity)
|
46
|
+
# @note defaults to Stern-Brocot range, (0..Infinity)
|
47
|
+
#
|
48
|
+
def range
|
49
|
+
(left_node..right_node)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Set the range of the tree
|
53
|
+
# @example
|
54
|
+
# FractionTree.range = :farey
|
55
|
+
# => (0/1..1/1)
|
56
|
+
# @note Accepts keywords:
|
57
|
+
# :farey, :keyboard, :scale_step, :log2 => (0/1..1/1)
|
58
|
+
# :stern_brocot, :scale => (0/1..1/0)
|
59
|
+
# :octave_reduced => (1/1..2/1)
|
60
|
+
#
|
61
|
+
def range=(rhs)
|
62
|
+
case rhs
|
63
|
+
when :farey, :keyboard, :scale_step, :log2
|
64
|
+
@left_node, @right_node = 0/1r, 1/1r
|
65
|
+
when :stern_brocot, :scale
|
66
|
+
@left_node, @right_node = 0/1r, Float::INFINITY
|
67
|
+
when :octave_reduced
|
68
|
+
@left_node, @right_node = 1/1r, 2/1r
|
69
|
+
else
|
70
|
+
@left_node = @right_node = nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# The cache of nodes used for faster lookup
|
75
|
+
# @note Intended for internal use.
|
76
|
+
def nodes
|
77
|
+
@@nodes ||= {}
|
78
|
+
end
|
79
|
+
|
80
|
+
# Reset the cache of nodes
|
81
|
+
# @example
|
82
|
+
# FractionTree.reset_nodes => {}
|
83
|
+
#
|
84
|
+
def reset_nodes
|
85
|
+
@@nodes = {}
|
86
|
+
end
|
87
|
+
|
88
|
+
# @return [FractionTree::Node] the node in the tree representing the given number
|
89
|
+
# @example
|
90
|
+
# FractionTree.node(3/2r) => (3/2)
|
91
|
+
#
|
92
|
+
def node(number)
|
93
|
+
validate(number)
|
94
|
+
nodes[number] ||= Node.new(number)
|
95
|
+
end
|
96
|
+
|
97
|
+
# @return [FractionTree::Node] the node decoded from the given string
|
98
|
+
# @example
|
99
|
+
# FractionTree.decode("RLL") => (4/3)
|
100
|
+
#
|
101
|
+
def decode(str)
|
102
|
+
wrk_node = Node.decode(str)
|
103
|
+
nodes[wrk_node.number] ||= wrk_node
|
104
|
+
end
|
105
|
+
|
106
|
+
# @return [FractionTree::Node] the mediant sum of the given numbers
|
107
|
+
# @example
|
108
|
+
# FractionTree.mediant_sum(3/2r, 4/3r) => (7/5)
|
109
|
+
#
|
110
|
+
def mediant_sum(n1, n2)
|
111
|
+
Node.new(n1) + Node.new(n2)
|
112
|
+
end
|
113
|
+
|
114
|
+
# @return [Boolean] whether two numbers are neighbors
|
115
|
+
# @example
|
116
|
+
# FractionTree.neighbors?(3/2r, 4/3r) => true
|
117
|
+
# FractionTree.neighbors?(3/2r, 7/4r) => false
|
118
|
+
# FractionTree.neighbors?(2/1r, Float::INFINITY) => true
|
119
|
+
# @param number1 of comparison
|
120
|
+
# @param number2 of comparison
|
121
|
+
# @note Neighbor definition: abs(a * d - b * c) = 1, for a/b, c/d
|
122
|
+
# @note Float::INFINITY => 1/0
|
123
|
+
#
|
124
|
+
def neighbors?(number1, number2)
|
125
|
+
(a, b) = number1.infinite? ? [1, 0] : [number1.numerator, number1.denominator]
|
126
|
+
(c, d) = number2.infinite? ? [1, 0] : [number2.numerator, number2.denominator]
|
127
|
+
(a * d - b * c).abs == 1
|
128
|
+
end
|
129
|
+
|
130
|
+
# @return [Array] a multi-dimensional array of fraction tree nodes
|
131
|
+
# @example
|
132
|
+
# FractionTree.tree(depth: 4)
|
133
|
+
# => [[(0/1), (1/0)],
|
134
|
+
# [(1/1)],
|
135
|
+
# [(1/2), (2/1)],
|
136
|
+
# [(1/3), (2/3), (3/2), (3/1)]]
|
137
|
+
#
|
138
|
+
# @param depth [Integer] the depth of the tree
|
139
|
+
# @param left_node [FractionTree::Node] the left starting node
|
140
|
+
# @param right_node [FractionTree::Node] the right starting node
|
141
|
+
#
|
142
|
+
def tree(depth: 10, left_node: default_left_node, right_node: default_right_node)
|
143
|
+
Array.new(depth, 0).tap do |sbt|
|
144
|
+
row = 0
|
145
|
+
sbt[row] = [left_node, right_node]
|
146
|
+
i = 2
|
147
|
+
while i <= depth do
|
148
|
+
figure_from = sbt[0..row].flatten.sort
|
149
|
+
new_frow = Array.new(2**(i-2), 0)
|
150
|
+
idx = 0
|
151
|
+
figure_from.each_cons(2) do |left, right|
|
152
|
+
new_frow[idx] = left + right
|
153
|
+
idx += 1
|
154
|
+
end
|
155
|
+
row += 1
|
156
|
+
sbt[row] = new_frow
|
157
|
+
i += 1
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# @return [FractionTree::Node] the mediant child of the given numbers
|
163
|
+
# @example
|
164
|
+
# FractionTree.child_of(1/1r, 4/3r) => (5/4)
|
165
|
+
# FractionTree.child_of(7/4r, 4/3r) => nil
|
166
|
+
#
|
167
|
+
# @param number1 [Rational] one of two parents
|
168
|
+
# @param number2 [Rational] two of two parents
|
169
|
+
# @note return nil if bc - ad |= 1, for a/b, c/d
|
170
|
+
#
|
171
|
+
def child_of(number1, number2)
|
172
|
+
return nil unless neighbors?(number1, number2)
|
173
|
+
# node(number1.numerator, number1.denominator) + node(number2.numerator, number2.denominator)
|
174
|
+
node(number1) + node(number2)
|
175
|
+
end
|
176
|
+
|
177
|
+
# @return [Array] of fraction tree nodes descended from parent1 and parent2
|
178
|
+
# Return empty array if bc - ad |= 1, for a/b, c/d
|
179
|
+
# @example
|
180
|
+
# FractionTree.descendants_of(1/1r, 4/3r)
|
181
|
+
# => [(1/1), (7/6), (6/5), (11/9), (5/4), (14/11), (9/7), (13/10), (4/3)]
|
182
|
+
#
|
183
|
+
# @param parent1 [Rational] one of two parents
|
184
|
+
# @param parent2 [Rational] two of two parents
|
185
|
+
# @param depth [Integer] the depth to collect
|
186
|
+
#
|
187
|
+
def descendants_of(parent1, parent2, depth: 5)
|
188
|
+
return [] unless neighbors?(parent1, parent2)
|
189
|
+
sequence(depth:, left_node: Node.new(parent1), right_node: Node.new(parent2))
|
190
|
+
end
|
191
|
+
|
192
|
+
# @return [Array] a sequence of fraction tree nodes
|
193
|
+
# @example
|
194
|
+
# FractionTree.new.sequence(3)
|
195
|
+
# => [(0/1), (1/3), (1/2), (2/3), (1/1), (3/2), (2/1), (3/1), (1/0)]
|
196
|
+
#
|
197
|
+
# @param depth [Integer] the number of iterations of the algorithm to run. The number of nodes returned will be greater
|
198
|
+
# @param left_node [FractionTree::Node] the left starting node
|
199
|
+
# @param right_node [FractionTree::Node] the right starting node
|
200
|
+
#
|
201
|
+
def sequence(depth: 5, left_node: default_left_node, right_node: default_right_node)
|
202
|
+
[left_node]+_sequence(depth:, left_node:, right_node:)+[right_node]
|
203
|
+
end
|
204
|
+
|
205
|
+
# @return [Array] of numerators of the fraction tree nodes. Aka the Stern-Brocot sequence.
|
206
|
+
# @example
|
207
|
+
# FractionTree.numeric_sequence.take(12)
|
208
|
+
# => [1, 1, 2, 1, 3, 2, 3, 1, 4, 3, 5, 2]
|
209
|
+
#
|
210
|
+
def numeric_sequence
|
211
|
+
return enum_for :numeric_sequence unless block_given?
|
212
|
+
a=[1,1]
|
213
|
+
|
214
|
+
0.step do |i|
|
215
|
+
yield a[i]
|
216
|
+
a << a[i]+a[i+1] << a[i+1]
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
private
|
221
|
+
def validate(num)
|
222
|
+
raise(ArgumentError, "#{num} not in range of #{range}", caller[0]) unless range.include?(num)
|
223
|
+
end
|
224
|
+
|
225
|
+
def default_left_node
|
226
|
+
node(left_node)
|
227
|
+
end
|
228
|
+
|
229
|
+
def default_right_node
|
230
|
+
node(right_node)
|
231
|
+
end
|
232
|
+
|
233
|
+
def _node(num, den=nil)
|
234
|
+
if num.kind_of?(Float)
|
235
|
+
num = num.to_d
|
236
|
+
end
|
237
|
+
if num.infinite?
|
238
|
+
num, den = 1, 0
|
239
|
+
end
|
240
|
+
if den.nil?
|
241
|
+
den = num.denominator
|
242
|
+
num = num.numerator
|
243
|
+
end
|
244
|
+
Node.new(num, den)
|
245
|
+
end
|
246
|
+
|
247
|
+
def _sequence(depth: 5, left_node:, right_node:)
|
248
|
+
return [] if depth == 0
|
249
|
+
|
250
|
+
mediant = left_node + right_node
|
251
|
+
|
252
|
+
# Generate left segment, mediant, then right segment
|
253
|
+
_sequence(depth: depth - 1, left_node:, right_node: mediant) + [mediant] + _sequence(depth: depth - 1, left_node: mediant, right_node:)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
@@ -0,0 +1,252 @@
|
|
1
|
+
# @author Jose Hales-Garcia
|
2
|
+
#
|
3
|
+
class FractionTree
|
4
|
+
class Node
|
5
|
+
extend Forwardable
|
6
|
+
include Comparable
|
7
|
+
|
8
|
+
def_delegators :@number, :zero?, :infinite?
|
9
|
+
|
10
|
+
attr_reader :numerator, :denominator, :number
|
11
|
+
|
12
|
+
IDENTITY_MATRIX = Matrix.identity(2)
|
13
|
+
LEFT_MATRIX = Matrix[[1,1],[0,1]]
|
14
|
+
RIGHT_MATRIX = Matrix[[1,0],[1,1]]
|
15
|
+
|
16
|
+
def initialize(num)
|
17
|
+
(@numerator, @denominator) = fraction_pair(num)
|
18
|
+
@number = num
|
19
|
+
end
|
20
|
+
|
21
|
+
alias :to_r :number
|
22
|
+
|
23
|
+
class << self
|
24
|
+
# @return [FractionTree::Node] the fraction decoded from the given string
|
25
|
+
# @example
|
26
|
+
# FractionTree::Node.decode("RLL") => (4/3)
|
27
|
+
#
|
28
|
+
def decode(string)
|
29
|
+
result = IDENTITY_MATRIX
|
30
|
+
|
31
|
+
string.split("").each do |direction|
|
32
|
+
case direction
|
33
|
+
when "L", "0", "l"
|
34
|
+
result = result * LEFT_MATRIX
|
35
|
+
when "R", "1", "r"
|
36
|
+
result = result * RIGHT_MATRIX
|
37
|
+
end
|
38
|
+
end
|
39
|
+
FractionTree.node(Rational(result.row(1).sum, result.row(0).sum))
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [String] the Stern-Brocot encoding of number
|
43
|
+
# @example
|
44
|
+
# FractionTree::Node.encode(4/3r) => "RLL"
|
45
|
+
# @param limit of codes to generate
|
46
|
+
#
|
47
|
+
def encode(number, limit: Float::INFINITY)
|
48
|
+
return nil if (number.infinite? || number.zero?)
|
49
|
+
|
50
|
+
m = number.numerator
|
51
|
+
n = number.denominator
|
52
|
+
|
53
|
+
return "I" if m == n
|
54
|
+
|
55
|
+
"".tap do |string|
|
56
|
+
while m != n && string.length < limit
|
57
|
+
if m < n
|
58
|
+
string << "L"
|
59
|
+
n = n - m
|
60
|
+
else
|
61
|
+
string << "R"
|
62
|
+
m = m - n
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# @return [Array] pair of numbers less and greater than the provided number by provided difference
|
69
|
+
# @example
|
70
|
+
# FractionTree::Node.plus_minus(3, 2) => [1, 5]
|
71
|
+
# @param
|
72
|
+
# num the base
|
73
|
+
# diff the number subtracted and added to base
|
74
|
+
#
|
75
|
+
def plus_minus(num, diff)
|
76
|
+
[num - diff, num + diff]
|
77
|
+
end
|
78
|
+
|
79
|
+
# @return [Integer] the decimal power of the provided number
|
80
|
+
# @example
|
81
|
+
# FractionTree::Node.decimal_power(1000) => 3
|
82
|
+
# @param
|
83
|
+
# logarithmand the number from which the log base 10 is obtained
|
84
|
+
#
|
85
|
+
def decimal_power(logarithmand)
|
86
|
+
Math.log10(logarithmand.abs).floor
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# @return [Array] set of fraction tree nodes leading to the given number
|
91
|
+
# @example
|
92
|
+
# FractionTree.node(7/4r).path
|
93
|
+
# => [(0/1), (1/0), (1/1), (2/1), (3/2), (5/3), (7/4)]
|
94
|
+
# @param limit of nodes to generate
|
95
|
+
#
|
96
|
+
def path(limit: Float::INFINITY)
|
97
|
+
return nil if infinite? || zero?
|
98
|
+
|
99
|
+
ln = tree.node(FractionTree.left_node)
|
100
|
+
rn = tree.node(FractionTree.right_node)
|
101
|
+
mn = ln + rn
|
102
|
+
return [ln, rn, mn] if mn == tree.node(number)
|
103
|
+
|
104
|
+
result = IDENTITY_MATRIX
|
105
|
+
m = numerator
|
106
|
+
n = denominator
|
107
|
+
[].tap do |p|
|
108
|
+
p << ln << rn << mn
|
109
|
+
while m != n && p.length < limit
|
110
|
+
if m < n
|
111
|
+
result = result * LEFT_MATRIX
|
112
|
+
n = n - m
|
113
|
+
else
|
114
|
+
result = result * RIGHT_MATRIX
|
115
|
+
m = m - n
|
116
|
+
end
|
117
|
+
p << tree.node(Rational(result.row(1).sum,result.row(0).sum))
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# @return [Array] a pair of fraction tree nodes leading to the given number.
|
123
|
+
# @example
|
124
|
+
# FractionTree.node(15/13r).parents => [(8/7), (7/6)]
|
125
|
+
# FractionTree.node(Math::PI).parents => [(1181999955934188/376242271442657), (1959592697655605/623757728557343)]
|
126
|
+
#
|
127
|
+
def parents
|
128
|
+
tmp = path
|
129
|
+
[tmp[-2], tmp[-2..-1].inject(&:-)].sort
|
130
|
+
end
|
131
|
+
|
132
|
+
# @return [Array] of [FractionTree::Node], sequence of Farey neighbors to self. A Farey neighbor is a number c/d, who's relationship to a/b is such that ad − bc = 1, when c/d < a/b and bc − ad = 1 when c/d > a/b.
|
133
|
+
# @example
|
134
|
+
# FractionTree.node(3/2r).neighbors(10)
|
135
|
+
# => [(1/1), (2/1), (4/3), (5/3), (7/5), (8/5), (10/7), (11/7), (13/9), (14/9)]
|
136
|
+
# @param r range of harmonic series to search
|
137
|
+
#
|
138
|
+
def neighbors(r = 10**(self.class.decimal_power(number.numerator)+2))
|
139
|
+
ratio = number.to_r
|
140
|
+
denominator = ratio.denominator
|
141
|
+
|
142
|
+
[].tap do |collection|
|
143
|
+
(1..r-1).each do |i|
|
144
|
+
lower, upper = self.class.plus_minus(ratio, Rational(1,i*denominator))
|
145
|
+
collection << tree.node(lower) if tree.neighbors?(ratio, lower)
|
146
|
+
collection << tree.node(upper) if tree.neighbors?(ratio, upper)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# @return [Array] the ancestors shared by self and the given number
|
152
|
+
# @example
|
153
|
+
# FractionTree.node(4/3r).common_ancestors_with(7/4r)
|
154
|
+
# => [(0/1), (1/0), (1/1), (2/1), (3/2)]
|
155
|
+
#
|
156
|
+
# @param num [Numeric] other number sharing descendants with self
|
157
|
+
#
|
158
|
+
def common_ancestors_with(num)
|
159
|
+
path & tree.node(num).path
|
160
|
+
end
|
161
|
+
|
162
|
+
# @return [Array] of fraction tree nodes, descending from parents of number
|
163
|
+
# @example
|
164
|
+
# FractionTree.node(5/4r).descendancy_from(depth: 3)
|
165
|
+
# => [(1/1), (7/6), (6/5), (11/9), (5/4), (14/11), (9/7), (13/10), (4/3)]
|
166
|
+
#
|
167
|
+
# @param depth [Integer] how many nodes to collect
|
168
|
+
#
|
169
|
+
def descendancy_from(depth: 5)
|
170
|
+
(parent1, parent2) = parents
|
171
|
+
tree.descendants_of(parent1.number, parent2.number, depth:)
|
172
|
+
end
|
173
|
+
|
174
|
+
# @return [FractionTree::Node] child of self and given number
|
175
|
+
# @example
|
176
|
+
# FractionTree.node(5/4r).child_with(4/3r)
|
177
|
+
# => (9/7)
|
178
|
+
# @note return nil if bc - ad |= 1, for a/b, c/d
|
179
|
+
#
|
180
|
+
def child_with(num)
|
181
|
+
tree.child_of(number, num)
|
182
|
+
end
|
183
|
+
|
184
|
+
# @return [String] encoding of self
|
185
|
+
# @example
|
186
|
+
# FractionTree.node(Math.log2(5/4r)).encoding(limit: 30) => "LLLRRRRRRRRRLLRRLLLLRRRRRRLLRL"
|
187
|
+
# @param limit of codes to generate
|
188
|
+
#
|
189
|
+
def encoding(limit: Float::INFINITY)
|
190
|
+
self.class.encode(number, limit:)
|
191
|
+
end
|
192
|
+
|
193
|
+
# @return [FractionTree::Node] sum of self and another node
|
194
|
+
# @example
|
195
|
+
# FractionTree.node(5/4r) + FractionTree.node(3/2r)
|
196
|
+
# => (4/3)
|
197
|
+
#
|
198
|
+
def +(rhs)
|
199
|
+
tree.node(Rational(self.numerator+rhs.numerator, self.denominator+rhs.denominator))
|
200
|
+
end
|
201
|
+
|
202
|
+
# @return [FractionTree::Node] difference of self and another node
|
203
|
+
# @example
|
204
|
+
# FractionTree.node(5/4r) - FractionTree.node(3/2r)
|
205
|
+
# => (1/1)
|
206
|
+
#
|
207
|
+
def -(rhs)
|
208
|
+
tree.node(Rational((self.numerator-rhs.numerator).abs, (self.denominator-rhs.denominator).abs))
|
209
|
+
end
|
210
|
+
|
211
|
+
def inspect
|
212
|
+
"(#{numerator}/#{denominator})"
|
213
|
+
end
|
214
|
+
alias :to_s :inspect
|
215
|
+
|
216
|
+
def <=>(rhs)
|
217
|
+
self.number <=> rhs.number
|
218
|
+
end
|
219
|
+
|
220
|
+
def ==(rhs)
|
221
|
+
self.number == rhs.number
|
222
|
+
end
|
223
|
+
|
224
|
+
# Needed for intersection operations to work.
|
225
|
+
# https://blog.mnishiguchi.com/ruby-intersection-of-object-arrays
|
226
|
+
# https://shortrecipes.blogspot.com/2006/10/ruby-intersection-of-two-arrays-of.html
|
227
|
+
# Also, allows using with Set, which uses Hash as storage and equality of its elements is determined according to Object#eql? and Object#hash.
|
228
|
+
#
|
229
|
+
def eql?(rhs)
|
230
|
+
rhs.instance_of?(self.class) && number == rhs.number
|
231
|
+
end
|
232
|
+
|
233
|
+
def hash
|
234
|
+
p, q = 17, 37
|
235
|
+
p = q * @id.hash
|
236
|
+
p = q * @name.hash
|
237
|
+
end
|
238
|
+
|
239
|
+
private
|
240
|
+
def tree
|
241
|
+
FractionTree
|
242
|
+
end
|
243
|
+
|
244
|
+
def fraction_pair(number)
|
245
|
+
if number.infinite?
|
246
|
+
[1, 0]
|
247
|
+
else
|
248
|
+
[number.numerator, number.denominator]
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
data/lib/fraction_tree.rb
CHANGED
@@ -1,325 +1,7 @@
|
|
1
|
+
require "matrix"
|
2
|
+
require "forwardable"
|
1
3
|
require "bigdecimal/util"
|
2
4
|
require "continued_fractions"
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
class FractionTree
|
7
|
-
DEFAULT_TREE_DEPTH = 20
|
8
|
-
|
9
|
-
class << self
|
10
|
-
# @return [Array] the boundary nodes of the tree
|
11
|
-
# @example
|
12
|
-
# FractionTree.base_segment => [(0/1), (1/0)]
|
13
|
-
#
|
14
|
-
def base_segment
|
15
|
-
[Node.new(0,1), Node.new(1,0)]
|
16
|
-
end
|
17
|
-
|
18
|
-
# @return [Array] a multi-dimensional array with the elements of fraction tree, organized by level/row
|
19
|
-
# @example
|
20
|
-
# FractionTree.tree(4)
|
21
|
-
# => [[(0/1), (1/0)],
|
22
|
-
# [(1/1)],
|
23
|
-
# [(1/2), (2/1)],
|
24
|
-
# [(1/3), (2/3), (3/2), (3/1)]]
|
25
|
-
#
|
26
|
-
# @param number [Integer] the depth of the tree
|
27
|
-
#
|
28
|
-
def tree(depth=DEFAULT_TREE_DEPTH)
|
29
|
-
Array.new(depth, 0).tap do |sbt|
|
30
|
-
row = 0
|
31
|
-
sbt[row] = base_segment
|
32
|
-
i = 2
|
33
|
-
while i <= depth do
|
34
|
-
figure_from = sbt[0..row].flatten.sort
|
35
|
-
new_frow = Array.new(2**(i-2), 0)
|
36
|
-
idx = 0
|
37
|
-
figure_from.each_cons(2) do |left,right|
|
38
|
-
new_frow[idx] = Node.new(left.numerator+right.numerator, left.denominator+right.denominator)
|
39
|
-
idx += 1
|
40
|
-
end
|
41
|
-
row += 1
|
42
|
-
sbt[row] = new_frow
|
43
|
-
i += 1
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
# @return [Array] a sequence of fraction tree nodes
|
49
|
-
# @example
|
50
|
-
# FractionTree.sequence(3)
|
51
|
-
# => [(0/1), (1/3), (1/2), (2/3), (1/1), (3/2), (2/1), (3/1), (1/0)]
|
52
|
-
#
|
53
|
-
# @param depth [Integer] the number of iterations of the algorithm to run. The number of nodes returned will be greater
|
54
|
-
# @param segment [Array] a tuple array of [FractionTree::Node] defining the segment of the tree to collect nodes.
|
55
|
-
#
|
56
|
-
def sequence(depth=5, segment: base_segment)
|
57
|
-
[segment.first]+_sequence(depth, segment:)+[segment.last]
|
58
|
-
end
|
59
|
-
|
60
|
-
# @return [Array] set of fraction nodes leading to the given number
|
61
|
-
# @example
|
62
|
-
# FractionTree.path_to(7/4r) => [(1/1), (2/1), (3/2), (5/3), (7/4)]
|
63
|
-
#
|
64
|
-
# @param number [Rational] the target the fraction path leads to
|
65
|
-
# @param find_parents [Boolean] list all ancestors or only immediate parents
|
66
|
-
# @param segment [Array] a tuple of [FractionTree::Node], defining the segment's starting left and right boundaries
|
67
|
-
#
|
68
|
-
def path_to(number, find_parents: false, segment: base_segment)
|
69
|
-
return Node.new(number.numerator, number.denominator) if number.zero?
|
70
|
-
number = number.kind_of?(Float) ? number.to_d : number
|
71
|
-
|
72
|
-
q = Node.new(number.numerator, number.denominator)
|
73
|
-
l = segment.first
|
74
|
-
h = segment.last
|
75
|
-
not_found = true
|
76
|
-
parents = []
|
77
|
-
results = []
|
78
|
-
while not_found
|
79
|
-
m = (l + h)
|
80
|
-
if m < q
|
81
|
-
l = m
|
82
|
-
elsif m > q
|
83
|
-
h = m
|
84
|
-
else
|
85
|
-
parents << l << h
|
86
|
-
not_found = false
|
87
|
-
end
|
88
|
-
results << m
|
89
|
-
end
|
90
|
-
find_parents == false ? results : parents
|
91
|
-
end
|
92
|
-
|
93
|
-
# @return [Array] a pair of fraction tree nodes leading to the given number.
|
94
|
-
# For irrational numbers, the parent nodes are one of an infinite series, whose nearness is determined by the limits of the system
|
95
|
-
# @example
|
96
|
-
# FractionTree.parents_of(15/13r) => [(8/7), (7/6)]
|
97
|
-
# FractionTree.parents_of(Math::PI) => [(447288330638589/142376297616907), (436991388364966/139098679093749)]
|
98
|
-
#
|
99
|
-
# @param number [Rational] the child number whose parents are being sought
|
100
|
-
#
|
101
|
-
def parents_of(number)
|
102
|
-
path_to(number, find_parents: true)
|
103
|
-
end
|
104
|
-
|
105
|
-
# @return [Array] the ancestors shared by the given descendants
|
106
|
-
# @example
|
107
|
-
# FractionTree.common_ancestors_between(4/3r, 7/4r)
|
108
|
-
# => [(1/1), (2/1), (3/2)]
|
109
|
-
#
|
110
|
-
# @param number1 [Rational] one of two descendants
|
111
|
-
# @param number2 [Rational] two of two descendants
|
112
|
-
#
|
113
|
-
def common_ancestors_between(number1, number2)
|
114
|
-
path_to(number1) & path_to(number2)
|
115
|
-
end
|
116
|
-
|
117
|
-
# @return [Array] the descendants of number starting at its parents
|
118
|
-
# @example
|
119
|
-
# FractionTree.descendancy_from(5/4r, 3)
|
120
|
-
# => [(1/1), (7/6), (6/5), (11/9), (5/4), (14/11), (9/7), (13/10), (4/3)]
|
121
|
-
#
|
122
|
-
# @param number [Rational] around which descendancy is focused
|
123
|
-
# @param depth [Integer] how many nodes to collect
|
124
|
-
#
|
125
|
-
def descendancy_from(number, depth=5)
|
126
|
-
parent1, parent2 = parents_of(number)
|
127
|
-
descendants_of(parent1, parent2, depth)
|
128
|
-
end
|
129
|
-
|
130
|
-
# @return [FractionTree::Node] the mediant child of the given numbers
|
131
|
-
# Return nil if bc - ad |= 1, for a/b, c/d
|
132
|
-
# @example
|
133
|
-
# FractionTree.child_of(1/1r, 4/3r) => (5/4)
|
134
|
-
# FractionTree.child_of(7/4r, 4/3r) => nil
|
135
|
-
#
|
136
|
-
# @param number1 [Rational] one of two parents
|
137
|
-
# @param number2 [Rational] two of two parents
|
138
|
-
# @param strict_neighbors [Boolean] whether to apply the strict Farey tree neighbor requirement
|
139
|
-
#
|
140
|
-
def child_of(number1, number2, strict_neighbors: true)
|
141
|
-
return nil unless farey_neighbors?(number1, number2) || !strict_neighbors
|
142
|
-
Node.new(number1.numerator, number1.denominator) + Node.new(number2.numerator, number2.denominator)
|
143
|
-
end
|
144
|
-
|
145
|
-
# @return [Array] of nodes descended from parent1 and parent2
|
146
|
-
# Return empty array if bc - ad |= 1, for a/b, c/d
|
147
|
-
# @example
|
148
|
-
# FractionTree.descendants_of(1/1r, 4/3r, 3)
|
149
|
-
# => [(1/1), (7/6), (6/5), (11/9), (5/4), (14/11), (9/7), (13/10), (4/3)]
|
150
|
-
#
|
151
|
-
# @param parent1 [Rational] one of two parents
|
152
|
-
# @param parent2 [Rational] two of two parents
|
153
|
-
# @param depth [Integer] the depth to collect
|
154
|
-
# @param strict_neighbors [Boolean] whether to apply the strict Farey tree neighbor requirement
|
155
|
-
#
|
156
|
-
def descendants_of(parent1, parent2, depth=5, strict_neighbors: true)
|
157
|
-
return [] unless farey_neighbors?(parent1, parent2) || !strict_neighbors
|
158
|
-
segment = [Node.new(parent1.numerator, parent1.denominator), Node.new(parent2.numerator, parent2.denominator)]
|
159
|
-
sequence(depth, segment: segment)
|
160
|
-
end
|
161
|
-
|
162
|
-
# @return [Array] of FractionTree::Nodes leading quotient-wise to number
|
163
|
-
# @example
|
164
|
-
# FractionTree.quotient_walk(15/13r)
|
165
|
-
# => [(1/1), (2/1), (3/2), (4/3), (5/4), (6/5), (7/6), (8/7), (15/13)]
|
166
|
-
#
|
167
|
-
# @param number [Numeric] to walk toward
|
168
|
-
# @param limit [Integer] the depth of the walk. Useful for irrational numbers
|
169
|
-
# @param segment [Array] the tuple of [FractionTree::Node] defining the segment of the tree
|
170
|
-
#
|
171
|
-
def quotient_walk(number, limit: 10, segment: computed_base_segment(number))
|
172
|
-
iterating_quotients = ContinuedFraction.new(number, limit).quotients.drop(1)
|
173
|
-
comparing_node = Node.new(number.numerator, number.denominator)
|
174
|
-
|
175
|
-
segment.tap do |arr|
|
176
|
-
held_node = arr[-2]
|
177
|
-
moving_node = arr[-1]
|
178
|
-
|
179
|
-
iterating_quotients.each do |q|
|
180
|
-
(1..q).each do |i|
|
181
|
-
arr << held_node + moving_node
|
182
|
-
# We don't want to walk past the number when it's a rational number and we've reached it
|
183
|
-
break if arr.include?(comparing_node)
|
184
|
-
moving_node = arr[-1]
|
185
|
-
end
|
186
|
-
held_node = arr[-2]
|
187
|
-
end
|
188
|
-
end
|
189
|
-
end
|
190
|
-
|
191
|
-
# @return [Array] of numerators of the fraction tree nodes. Also known as the Stern-Brocot sequence.
|
192
|
-
# @example
|
193
|
-
# FractionTree.numeric_sequence.take(12)
|
194
|
-
# => [1, 1, 2, 1, 3, 2, 3, 1, 4, 3, 5, 2]
|
195
|
-
#
|
196
|
-
def numeric_sequence
|
197
|
-
return enum_for :numeric_sequence unless block_given?
|
198
|
-
a=[1,1]
|
199
|
-
|
200
|
-
0.step do |i|
|
201
|
-
yield a[i]
|
202
|
-
a << a[i]+a[i+1] << a[i+1]
|
203
|
-
end
|
204
|
-
end
|
205
|
-
|
206
|
-
# @return [Array] of Farey neighbors to the given number. A Farey neighbor is a number b/c, who's relationship to a/b is such that ad − bc = 1, when c/d < a/b and bc − ad = 1 when c/d > a/b.
|
207
|
-
# @example
|
208
|
-
# FractionTree.farey_neighbors(3/2r, 10)
|
209
|
-
# => [(1/1), (2/1), (4/3), (5/3), (7/5), (8/5), (10/7), (11/7), (13/9), (14/9)]
|
210
|
-
# @param number with neighbors
|
211
|
-
# @param range of harmonic series to search
|
212
|
-
#
|
213
|
-
def farey_neighbors(number, range = 10**(decimal_power(number.numerator)+2))
|
214
|
-
ratio = number.to_r
|
215
|
-
denominator = ratio.denominator
|
216
|
-
|
217
|
-
[].tap do |collection|
|
218
|
-
(1..range-1).each do |i|
|
219
|
-
lower, upper = plus_minus(ratio, Rational(1,i*denominator))
|
220
|
-
collection << lower if farey_neighbors?(ratio, lower)
|
221
|
-
collection << upper if farey_neighbors?(ratio, upper)
|
222
|
-
end
|
223
|
-
end
|
224
|
-
end
|
225
|
-
|
226
|
-
# @return [Boolean] whether two numbers are Farey neighbors
|
227
|
-
# @example
|
228
|
-
# FractionTree.farey_neighbors?(3/2r, 4/3r) => true
|
229
|
-
# FractionTree.farey_neighbors?(3/2r, 7/4r) => false
|
230
|
-
# @param num1 of comparison
|
231
|
-
# @param num2 of comparison
|
232
|
-
#
|
233
|
-
def farey_neighbors?(num1, num2)
|
234
|
-
(num1.numerator * num2.denominator - num1.denominator * num2.numerator).abs == 1
|
235
|
-
end
|
236
|
-
|
237
|
-
private
|
238
|
-
def computed_base_segment(number)
|
239
|
-
floor = number.floor
|
240
|
-
[Node.new(floor,1), Node.new(floor+1,1)]
|
241
|
-
end
|
242
|
-
|
243
|
-
def _sequence(depth = 5, segment:)
|
244
|
-
return [] if depth == 0
|
245
|
-
|
246
|
-
mediant = segment.first + segment.last
|
247
|
-
|
248
|
-
# Generate left segment, mediant, then right segment
|
249
|
-
_sequence(depth - 1, segment: [segment.first, mediant]) + [mediant] + _sequence(depth - 1, segment: [mediant, segment.last])
|
250
|
-
end
|
251
|
-
|
252
|
-
def plus_minus(number, diff)
|
253
|
-
[number - diff, number + diff]
|
254
|
-
end
|
255
|
-
|
256
|
-
def decimal_power(number)
|
257
|
-
Math.log10(number.abs).floor
|
258
|
-
end
|
259
|
-
end
|
260
|
-
|
261
|
-
class Node
|
262
|
-
include Comparable
|
263
|
-
|
264
|
-
attr_reader :numerator, :denominator, :weight
|
265
|
-
|
266
|
-
def initialize(n,d)
|
267
|
-
@numerator = n
|
268
|
-
@denominator = d
|
269
|
-
@weight = (d == 0 ? Float::INFINITY : Rational(@numerator, @denominator))
|
270
|
-
end
|
271
|
-
|
272
|
-
alias :to_r :weight
|
273
|
-
|
274
|
-
def inspect
|
275
|
-
"(#{numerator}/#{denominator})"
|
276
|
-
end
|
277
|
-
alias :to_s :inspect
|
278
|
-
|
279
|
-
def <=>(rhs)
|
280
|
-
self.weight <=> rhs.weight
|
281
|
-
end
|
282
|
-
|
283
|
-
def ==(rhs)
|
284
|
-
self.weight == rhs.weight
|
285
|
-
end
|
286
|
-
|
287
|
-
# Needed for intersection operations to work.
|
288
|
-
# https://blog.mnishiguchi.com/ruby-intersection-of-object-arrays
|
289
|
-
# https://shortrecipes.blogspot.com/2006/10/ruby-intersection-of-two-arrays-of.html
|
290
|
-
# Also, allows using with Set, which uses Hash as storage and equality of its elements is determined according to Object#eql? and Object#hash.
|
291
|
-
#
|
292
|
-
def eql?(rhs)
|
293
|
-
rhs.instance_of?(self.class) && weight == rhs.weight
|
294
|
-
end
|
295
|
-
|
296
|
-
def hash
|
297
|
-
p, q = 17, 37
|
298
|
-
p = q * @id.hash
|
299
|
-
p = q * @name.hash
|
300
|
-
end
|
301
|
-
|
302
|
-
def +(rhs)
|
303
|
-
self.class.new(self.numerator+rhs.numerator, self.denominator+rhs.denominator)
|
304
|
-
end
|
305
|
-
end
|
306
|
-
end
|
307
|
-
|
308
|
-
class SternBrocotTree < FractionTree; end
|
309
|
-
class ScaleTree < FractionTree; end
|
310
|
-
|
311
|
-
class OctaveReducedTree < FractionTree
|
312
|
-
def self.base_segment
|
313
|
-
[Node.new(1,1), Node.new(2,1)]
|
314
|
-
end
|
315
|
-
end
|
316
|
-
|
317
|
-
class FareyTree < FractionTree
|
318
|
-
def self.base_segment
|
319
|
-
[Node.new(0,1), Node.new(1,1)]
|
320
|
-
end
|
321
|
-
end
|
322
|
-
|
323
|
-
class KeyboardTree < FareyTree; end
|
324
|
-
class ScaleStepTree < FareyTree; end
|
325
|
-
class Log2Tree < FareyTree; end
|
5
|
+
require_relative "fraction_tree/node"
|
6
|
+
require_relative "fraction_tree/extensions"
|
7
|
+
require_relative "fraction_tree/fraction_tree"
|
metadata
CHANGED
@@ -1,15 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fraction-tree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jose Hales-Garcia
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-09-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: matrix
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.4'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.4'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: forwardable
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.3'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.3'
|
13
41
|
- !ruby/object:Gem::Dependency
|
14
42
|
name: continued_fractions
|
15
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -87,6 +115,9 @@ extensions: []
|
|
87
115
|
extra_rdoc_files: []
|
88
116
|
files:
|
89
117
|
- lib/fraction_tree.rb
|
118
|
+
- lib/fraction_tree/extensions.rb
|
119
|
+
- lib/fraction_tree/fraction_tree.rb
|
120
|
+
- lib/fraction_tree/node.rb
|
90
121
|
homepage: https://jolohaga.github.io/fraction-tree/
|
91
122
|
licenses:
|
92
123
|
- MIT
|
@@ -107,7 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
107
138
|
- !ruby/object:Gem::Version
|
108
139
|
version: '3.1'
|
109
140
|
requirements: []
|
110
|
-
rubygems_version: 3.5.
|
141
|
+
rubygems_version: 3.5.20
|
111
142
|
signing_key:
|
112
143
|
specification_version: 4
|
113
144
|
summary: Fraction tree
|