fraction-tree 0.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 +7 -0
- data/lib/fraction_tree.rb +271 -0
- metadata +99 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f172fb267db51a11191c850ac073849bd642cd831cafc1c370d6837757fb3138
|
4
|
+
data.tar.gz: db36bc04f19e0d7affc3af6c7eba4e9dcdf03f4fe7ac835764e5dfe45655aa39
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 27bdbb1bf2f250cc37d111a0df925c70c557c8ea061ce534e7bc94c23631f2be7c04a866d83011771cb6831b82459c7944790c3209d1ffa4c350843eaeb0a063
|
7
|
+
data.tar.gz: 3cb3f3b9ca09339d72ce08f5e69a96aebf20de27bffcc107d6fb3321ca5b7fb9dd6f198ac1d6166558a838ff2ee8a0c8e33ff317d97e957507b17b3cf13ee097
|
@@ -0,0 +1,271 @@
|
|
1
|
+
require "continued_fractions"
|
2
|
+
|
3
|
+
class FractionTree
|
4
|
+
DEFAULT_TREE_DEPTH = 20
|
5
|
+
|
6
|
+
class << self
|
7
|
+
# @return [Array] the boundary nodes of the tree
|
8
|
+
# @example
|
9
|
+
# FractionTree.base_segment => [(0/1), (1/0)]
|
10
|
+
#
|
11
|
+
def base_segment
|
12
|
+
[Node.new(0,1), Node.new(1,0)]
|
13
|
+
end
|
14
|
+
|
15
|
+
# @return [Array] a multi-dimensional array with the elements of fraction tree, organized by level/row
|
16
|
+
# @example
|
17
|
+
# FractionTree.tree(4)
|
18
|
+
# => [[(0/1), (1/0)],
|
19
|
+
# [(1/1)],
|
20
|
+
# [(1/2), (2/1)],
|
21
|
+
# [(1/3), (2/3), (3/2), (3/1)]]
|
22
|
+
#
|
23
|
+
# @param number [Integer] the depth of the tree
|
24
|
+
#
|
25
|
+
def tree(depth=DEFAULT_TREE_DEPTH)
|
26
|
+
Array.new(depth, 0).tap do |sbt|
|
27
|
+
row = 0
|
28
|
+
sbt[row] = base_segment
|
29
|
+
i = 2
|
30
|
+
while i <= depth do
|
31
|
+
figure_from = sbt[0..row].flatten.sort
|
32
|
+
new_frow = Array.new(2**(i-2), 0)
|
33
|
+
idx = 0
|
34
|
+
figure_from.each_cons(2) do |left,right|
|
35
|
+
new_frow[idx] = Node.new(left.numerator+right.numerator, left.denominator+right.denominator)
|
36
|
+
idx += 1
|
37
|
+
end
|
38
|
+
row += 1
|
39
|
+
sbt[row] = new_frow
|
40
|
+
i += 1
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [Array] a sequence of fraction tree nodes
|
46
|
+
# @example
|
47
|
+
# FractionTree.sequence(3)
|
48
|
+
# => [(0/1), (1/3), (1/2), (2/3), (1/1), (3/2), (2/1), (3/1), (1/0)]
|
49
|
+
#
|
50
|
+
# @param depth the number of iterations of the algorithm to run. The number of nodes returned will be greater
|
51
|
+
# @param segment a tuple array defining the segment of the tree to collect nodes. The elements of the tuple must be instances of FractionTree::Node
|
52
|
+
#
|
53
|
+
def sequence(depth=5, segment: base_segment)
|
54
|
+
[segment.first]+_sequence(depth, segment:)+[segment.last]
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [Array] set of fraction nodes leading to the given number
|
58
|
+
# @example
|
59
|
+
# FractionTree.path_to(7/4r) => [(1/1), (2/1), (3/2), (5/3), (7/4)]
|
60
|
+
#
|
61
|
+
# @param number the target the fraction path leads to
|
62
|
+
#
|
63
|
+
def path_to(number, find_parents: false, segment: base_segment)
|
64
|
+
return Node.new(number.numerator, number.denominator) if number.zero?
|
65
|
+
q = Node.new(number.numerator, number.denominator)
|
66
|
+
l = segment.first
|
67
|
+
h = segment.last
|
68
|
+
not_found = true
|
69
|
+
parents = []
|
70
|
+
results = []
|
71
|
+
while not_found
|
72
|
+
m = (l + h)
|
73
|
+
if m < q
|
74
|
+
l = m
|
75
|
+
elsif m > q
|
76
|
+
h = m
|
77
|
+
else
|
78
|
+
parents << l << h
|
79
|
+
not_found = false
|
80
|
+
end
|
81
|
+
results << m
|
82
|
+
end
|
83
|
+
find_parents == false ? results : parents
|
84
|
+
end
|
85
|
+
|
86
|
+
# @return [Array] a pair of fraction tree nodes leading to the given number.
|
87
|
+
# For irrational numbers, the parent nodes are one of an infinite series, whose nearness is determined by the limits of the system
|
88
|
+
# @example
|
89
|
+
# FractionTree.parents_of(15/13r) => [(8/7), (7/6)]
|
90
|
+
# FractionTree.parents_of(Math::PI) => [(447288330638589/142376297616907), (436991388364966/139098679093749)]
|
91
|
+
#
|
92
|
+
# @param num the child number whose parents are being sought
|
93
|
+
#
|
94
|
+
def parents_of(number)
|
95
|
+
path_to(number, find_parents: true)
|
96
|
+
end
|
97
|
+
|
98
|
+
# @return [Array] the ancestors shared by the given descendants
|
99
|
+
# @example
|
100
|
+
# FractionTree.common_ancestors_between(4/3r, 7/4r)
|
101
|
+
# => [(1/1), (2/1), (3/2)]
|
102
|
+
#
|
103
|
+
# @param number1 one of two descendants
|
104
|
+
# @param number2 two of two descendants
|
105
|
+
#
|
106
|
+
def common_ancestors_between(number1, number2)
|
107
|
+
path_to(number1) & path_to(number2)
|
108
|
+
end
|
109
|
+
|
110
|
+
# @return [Array] the descendants of number starting at its parents
|
111
|
+
# @example
|
112
|
+
# FractionTree.descendancy_from(5/4r, 3)
|
113
|
+
# => [(1/1), (7/6), (6/5), (11/9), (5/4), (14/11), (9/7), (13/10), (4/3)]
|
114
|
+
#
|
115
|
+
# @param number around which descendancy is focused
|
116
|
+
# @param depth how many nodes to collect
|
117
|
+
#
|
118
|
+
def descendancy_from(number, depth=5)
|
119
|
+
parent1, parent2 = parents_of(number)
|
120
|
+
descendants_of(parent1, parent2, depth)
|
121
|
+
end
|
122
|
+
|
123
|
+
# @return [FractionTree::Node] the mediant child of the given numbers
|
124
|
+
# Return nil if bc - ad |= 1, for a/b, c/d
|
125
|
+
# @example
|
126
|
+
# FractionTree.child_of(1/1r, 4/3r) => (5/4)
|
127
|
+
# FractionTree.child_of(7/4r, 4/3r) => nil
|
128
|
+
#
|
129
|
+
# @param number1 one of two parents
|
130
|
+
# @param number2 two of two parents
|
131
|
+
#
|
132
|
+
def child_of(number1, number2, strict_neighbors: true)
|
133
|
+
return nil unless farey_neighbors?(number1, number2) || !strict_neighbors
|
134
|
+
Node.new(number1.numerator, number1.denominator) + Node.new(number2.numerator, number2.denominator)
|
135
|
+
end
|
136
|
+
|
137
|
+
# @return [Array] of nodes descended from parent1 and parent2
|
138
|
+
# Return empty array if bc - ad |= 1, for a/b, c/d
|
139
|
+
# @example
|
140
|
+
# FractionTree.descendants_of(1/1r, 4/3r, 3)
|
141
|
+
# => [(1/1), (7/6), (6/5), (11/9), (5/4), (14/11), (9/7), (13/10), (4/3)]
|
142
|
+
#
|
143
|
+
# @param parent1 one of two parents
|
144
|
+
# @param parent2 two of two parents
|
145
|
+
#
|
146
|
+
def descendants_of(parent1, parent2, depth=5, strict_neighbors: true)
|
147
|
+
return [] unless farey_neighbors?(parent1, parent2) || !strict_neighbors
|
148
|
+
segment = [Node.new(parent1.numerator, parent1.denominator), Node.new(parent2.numerator, parent2.denominator)]
|
149
|
+
sequence(depth, segment: segment)
|
150
|
+
end
|
151
|
+
|
152
|
+
# @return [Array] of FractionTree::Nodes leading quotient-wise to number
|
153
|
+
# @example
|
154
|
+
# FractionTree.quotient_walk(15/13r)
|
155
|
+
# => [(1/1), (2/1), (3/2), (4/3), (5/4), (6/5), (7/6), (8/7), (15/13)]
|
156
|
+
#
|
157
|
+
# @param number to walk toward
|
158
|
+
# @param limit the depth of the walk. Useful for irrational numbers
|
159
|
+
#
|
160
|
+
def quotient_walk(number, limit: 10, segment: computed_base_segment(number))
|
161
|
+
iterating_quotients = ContinuedFraction.new(number, limit).quotients.drop(1)
|
162
|
+
comparing_node = Node.new(number.numerator, number.denominator)
|
163
|
+
|
164
|
+
segment.tap do |arr|
|
165
|
+
held_node = arr[-2]
|
166
|
+
moving_node = arr[-1]
|
167
|
+
|
168
|
+
iterating_quotients.each do |q|
|
169
|
+
(1..q).each do |i|
|
170
|
+
arr << held_node + moving_node
|
171
|
+
# We don't want to walk past the number when it's a rational number and we've reached it
|
172
|
+
break if arr.include?(comparing_node)
|
173
|
+
moving_node = arr[-1]
|
174
|
+
end
|
175
|
+
held_node = arr[-2]
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# @return [Array] of numerators of the fraction tree nodes. Also known as the Stern-Brocot sequence.
|
181
|
+
# @example
|
182
|
+
# FractionTree.numeric_sequence.take(12)
|
183
|
+
# => [1, 1, 2, 1, 3, 2, 3, 1, 4, 3, 5, 2]
|
184
|
+
#
|
185
|
+
def numeric_sequence
|
186
|
+
return enum_for :numeric_sequence unless block_given?
|
187
|
+
a=[1,1]
|
188
|
+
|
189
|
+
0.step do |i|
|
190
|
+
yield a[i]
|
191
|
+
a << a[i]+a[i+1] << a[i+1]
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
private
|
196
|
+
def computed_base_segment(number)
|
197
|
+
floor = number.floor
|
198
|
+
[Node.new(floor,1), Node.new(floor+1,1)]
|
199
|
+
end
|
200
|
+
|
201
|
+
def _sequence(depth = 5, segment:)
|
202
|
+
return [] if depth == 0
|
203
|
+
|
204
|
+
mediant = segment.first + segment.last
|
205
|
+
|
206
|
+
# Generate left segment, mediant, then right segment
|
207
|
+
_sequence(depth - 1, segment: [segment.first, mediant]) + [mediant] + _sequence(depth - 1, segment: [mediant, segment.last])
|
208
|
+
end
|
209
|
+
|
210
|
+
def farey_neighbors?(num1, num2)
|
211
|
+
(num1.numerator * num2.denominator - num1.denominator * num2.numerator).abs == 1
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
class Node
|
216
|
+
include Comparable
|
217
|
+
|
218
|
+
attr_accessor :numerator, :denominator, :weight
|
219
|
+
|
220
|
+
def initialize(n,d)
|
221
|
+
@numerator = n
|
222
|
+
@denominator = d
|
223
|
+
@weight = (d == 0 ? Float::INFINITY : Rational(@numerator, @denominator))
|
224
|
+
end
|
225
|
+
|
226
|
+
alias :to_r :weight
|
227
|
+
|
228
|
+
def inspect
|
229
|
+
"(#{numerator}/#{denominator})"
|
230
|
+
end
|
231
|
+
alias :to_s :inspect
|
232
|
+
|
233
|
+
def <=>(rhs)
|
234
|
+
self.weight <=> rhs.weight
|
235
|
+
end
|
236
|
+
|
237
|
+
def ==(rhs)
|
238
|
+
self.weight == rhs.weight
|
239
|
+
end
|
240
|
+
|
241
|
+
# Needed for intersection operations to work.
|
242
|
+
# https://blog.mnishiguchi.com/ruby-intersection-of-object-arrays
|
243
|
+
# https://shortrecipes.blogspot.com/2006/10/ruby-intersection-of-two-arrays-of.html
|
244
|
+
def eql?(rhs)
|
245
|
+
rhs.kind_of?(self.class) && weight == rhs.weight
|
246
|
+
end
|
247
|
+
|
248
|
+
def +(rhs)
|
249
|
+
self.class.new(self.numerator+rhs.numerator, self.denominator+rhs.denominator)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
class SternBrocotTree < FractionTree; end
|
255
|
+
class ScaleTree < FractionTree; end
|
256
|
+
|
257
|
+
class OctaveReducedTree < FractionTree
|
258
|
+
def self.base_segment
|
259
|
+
[Node.new(1,1), Node.new(2,1)]
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
class FareyTree < FractionTree
|
264
|
+
def self.base_segment
|
265
|
+
[Node.new(0,1), Node.new(1,1)]
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
class KeyboardTree < FareyTree; end
|
270
|
+
class ScaleStepTree < FareyTree; end
|
271
|
+
class Log2Tree < FareyTree; end
|
metadata
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fraction-tree
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jose Hales-Garcia
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-09-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: continued_fractions
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.8'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.8'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.2'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: byebug
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '11.1'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '11.1'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: yard
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.9'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.9'
|
69
|
+
description: A collection of Stern-Brocot based models and methods
|
70
|
+
email: jose@halesgarcia.com
|
71
|
+
executables: []
|
72
|
+
extensions: []
|
73
|
+
extra_rdoc_files: []
|
74
|
+
files:
|
75
|
+
- lib/fraction_tree.rb
|
76
|
+
homepage: https://jolohaga.github.io/fraction-tree/
|
77
|
+
licenses:
|
78
|
+
- MIT
|
79
|
+
metadata: {}
|
80
|
+
post_install_message:
|
81
|
+
rdoc_options: []
|
82
|
+
require_paths:
|
83
|
+
- lib
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
requirements: []
|
95
|
+
rubygems_version: 3.4.19
|
96
|
+
signing_key:
|
97
|
+
specification_version: 4
|
98
|
+
summary: Fraction tree
|
99
|
+
test_files: []
|