fractional_indexing 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1ec000613bb56c79ff44198f1fc0e45be734922f89c22b5ce9fb5ec5ec0ae2df
4
+ data.tar.gz: ead7373a2c426ff75274a5b0fb87cf3f5e8332075ae4e2c70fa071d1795c5ea8
5
+ SHA512:
6
+ metadata.gz: 8914f607fd7028a06c1082901baa30aba1330f354643af03cf1a2b99985cd239378ba96aebf17c8a241bf2dbab2550fb9111086047e45456998a07392e1494af
7
+ data.tar.gz: 2b2996841ca207d20a26670d30f4dffeef721334ab6ceb2a5143abd543f400ee43b8289867c5057c8827e4f5ddf7d934003316ca13cccad133b7c6b14c5a5cff
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in fractional_indexing.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "minitest", "~> 5.0"
data/Gemfile.lock ADDED
@@ -0,0 +1,21 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ fractional_indexing (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ minitest (5.18.0)
10
+ rake (13.0.6)
11
+
12
+ PLATFORMS
13
+ x86_64-darwin-20
14
+
15
+ DEPENDENCIES
16
+ fractional_indexing!
17
+ minitest (~> 5.0)
18
+ rake (~> 13.0)
19
+
20
+ BUNDLED WITH
21
+ 2.4.6
data/LICENSE ADDED
@@ -0,0 +1,121 @@
1
+ Creative Commons Legal Code
2
+
3
+ CC0 1.0 Universal
4
+
5
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
6
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
7
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
8
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
9
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
10
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
11
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
12
+ HEREUNDER.
13
+
14
+ Statement of Purpose
15
+
16
+ The laws of most jurisdictions throughout the world automatically confer
17
+ exclusive Copyright and Related Rights (defined below) upon the creator
18
+ and subsequent owner(s) (each and all, an "owner") of an original work of
19
+ authorship and/or a database (each, a "Work").
20
+
21
+ Certain owners wish to permanently relinquish those rights to a Work for
22
+ the purpose of contributing to a commons of creative, cultural and
23
+ scientific works ("Commons") that the public can reliably and without fear
24
+ of later claims of infringement build upon, modify, incorporate in other
25
+ works, reuse and redistribute as freely as possible in any form whatsoever
26
+ and for any purposes, including without limitation commercial purposes.
27
+ These owners may contribute to the Commons to promote the ideal of a free
28
+ culture and the further production of creative, cultural and scientific
29
+ works, or to gain reputation or greater distribution for their Work in
30
+ part through the use and efforts of others.
31
+
32
+ For these and/or other purposes and motivations, and without any
33
+ expectation of additional consideration or compensation, the person
34
+ associating CC0 with a Work (the "Affirmer"), to the extent that he or she
35
+ is an owner of Copyright and Related Rights in the Work, voluntarily
36
+ elects to apply CC0 to the Work and publicly distribute the Work under its
37
+ terms, with knowledge of his or her Copyright and Related Rights in the
38
+ Work and the meaning and intended legal effect of CC0 on those rights.
39
+
40
+ 1. Copyright and Related Rights. A Work made available under CC0 may be
41
+ protected by copyright and related or neighboring rights ("Copyright and
42
+ Related Rights"). Copyright and Related Rights include, but are not
43
+ limited to, the following:
44
+
45
+ i. the right to reproduce, adapt, distribute, perform, display,
46
+ communicate, and translate a Work;
47
+ ii. moral rights retained by the original author(s) and/or performer(s);
48
+ iii. publicity and privacy rights pertaining to a person's image or
49
+ likeness depicted in a Work;
50
+ iv. rights protecting against unfair competition in regards to a Work,
51
+ subject to the limitations in paragraph 4(a), below;
52
+ v. rights protecting the extraction, dissemination, use and reuse of data
53
+ in a Work;
54
+ vi. database rights (such as those arising under Directive 96/9/EC of the
55
+ European Parliament and of the Council of 11 March 1996 on the legal
56
+ protection of databases, and under any national implementation
57
+ thereof, including any amended or successor version of such
58
+ directive); and
59
+ vii. other similar, equivalent or corresponding rights throughout the
60
+ world based on applicable law or treaty, and any national
61
+ implementations thereof.
62
+
63
+ 2. Waiver. To the greatest extent permitted by, but not in contravention
64
+ of, applicable law, Affirmer hereby overtly, fully, permanently,
65
+ irrevocably and unconditionally waives, abandons, and surrenders all of
66
+ Affirmer's Copyright and Related Rights and associated claims and causes
67
+ of action, whether now known or unknown (including existing as well as
68
+ future claims and causes of action), in the Work (i) in all territories
69
+ worldwide, (ii) for the maximum duration provided by applicable law or
70
+ treaty (including future time extensions), (iii) in any current or future
71
+ medium and for any number of copies, and (iv) for any purpose whatsoever,
72
+ including without limitation commercial, advertising or promotional
73
+ purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
74
+ member of the public at large and to the detriment of Affirmer's heirs and
75
+ successors, fully intending that such Waiver shall not be subject to
76
+ revocation, rescission, cancellation, termination, or any other legal or
77
+ equitable action to disrupt the quiet enjoyment of the Work by the public
78
+ as contemplated by Affirmer's express Statement of Purpose.
79
+
80
+ 3. Public License Fallback. Should any part of the Waiver for any reason
81
+ be judged legally invalid or ineffective under applicable law, then the
82
+ Waiver shall be preserved to the maximum extent permitted taking into
83
+ account Affirmer's express Statement of Purpose. In addition, to the
84
+ extent the Waiver is so judged Affirmer hereby grants to each affected
85
+ person a royalty-free, non transferable, non sublicensable, non exclusive,
86
+ irrevocable and unconditional license to exercise Affirmer's Copyright and
87
+ Related Rights in the Work (i) in all territories worldwide, (ii) for the
88
+ maximum duration provided by applicable law or treaty (including future
89
+ time extensions), (iii) in any current or future medium and for any number
90
+ of copies, and (iv) for any purpose whatsoever, including without
91
+ limitation commercial, advertising or promotional purposes (the
92
+ "License"). The License shall be deemed effective as of the date CC0 was
93
+ applied by Affirmer to the Work. Should any part of the License for any
94
+ reason be judged legally invalid or ineffective under applicable law, such
95
+ partial invalidity or ineffectiveness shall not invalidate the remainder
96
+ of the License, and in such case Affirmer hereby affirms that he or she
97
+ will not (i) exercise any of his or her remaining Copyright and Related
98
+ Rights in the Work or (ii) assert any associated claims and causes of
99
+ action with respect to the Work, in either case contrary to Affirmer's
100
+ express Statement of Purpose.
101
+
102
+ 4. Limitations and Disclaimers.
103
+
104
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
105
+ surrendered, licensed or otherwise affected by this document.
106
+ b. Affirmer offers the Work as-is and makes no representations or
107
+ warranties of any kind concerning the Work, express, implied,
108
+ statutory or otherwise, including without limitation warranties of
109
+ title, merchantability, fitness for a particular purpose, non
110
+ infringement, or the absence of latent or other defects, accuracy, or
111
+ the present or absence of errors, whether or not discoverable, all to
112
+ the greatest extent permissible under applicable law.
113
+ c. Affirmer disclaims responsibility for clearing rights of other persons
114
+ that may apply to the Work or any use thereof, including without
115
+ limitation any person's Copyright and Related Rights in the Work.
116
+ Further, Affirmer disclaims responsibility for obtaining any necessary
117
+ consents, permissions or other rights required for any use of the
118
+ Work.
119
+ d. Affirmer understands and acknowledges that Creative Commons is not a
120
+ party to this document and has no duty or obligation with respect to
121
+ this CC0 or use of the Work.
data/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # Fractional Indexing
2
+
3
+ This is based on [Implementing Fractional Indexing
4
+ ](https://observablehq.com/@dgreensp/implementing-fractional-indexing) by [David Greenspan
5
+ ](https://github.com/dgreensp).
6
+
7
+ Fractional indexing is a technique to create an ordering that can be used for [Realtime Editing of Ordered Sequences](https://www.figma.com/blog/realtime-editing-of-ordered-sequences/).
8
+
9
+ This implementation includes variable-length integers, and the prepend/append optimization described in David's article.
10
+
11
+ ## Installation
12
+
13
+ Install the gem and add to the application's Gemfile by executing:
14
+
15
+ $ bundle add fractional_indexing
16
+
17
+ If bundler is not being used to manage dependencies, install the gem by executing:
18
+
19
+ $ gem install fractional_indexing
20
+
21
+ ## Usage
22
+
23
+ ```ruby
24
+ require "fractional_indexing"
25
+
26
+ first = FractionalIndexing.generate_key_between(None, None)
27
+ raise unless first == 'a0'
28
+
29
+ # Insert after 1st
30
+ second = FractionalIndexing.generate_key_between(first, None)
31
+ raise unless second == 'a1'
32
+
33
+ # Insert after 2nd
34
+ third = FractionalIndexing.generate_key_between(second, None)
35
+ raise unless third == 'a2'
36
+
37
+ # Insert before 1st
38
+ zeroth = FractionalIndexing.generate_key_between(None, first)
39
+ raise unless zeroth == 'Zz'
40
+
41
+ # Insert in between 2nd and 3rd. Midpoint
42
+ second_and_half = FractionalIndexing.generate_key_between(second, third)
43
+ raise unless second_and_half == 'a1V'
44
+ ```
45
+
46
+ ## Other Languages
47
+
48
+ This is a Ruby port of the [Python port](https://github.com/httpie/fractional-indexing-python) of the [original JavaScript implementation](https://github.com/rocicorp/fractional-indexing).
49
+ That means that this implementation is byte-for-byte compatible with:
50
+
51
+ | Language | Repo |
52
+ | ---------- | ---------------------------------------------------- |
53
+ | JavaScript | https://github.com/rocicorp/fractional-indexing |
54
+ | Go | https://github.com/rocicorp/fracdex |
55
+ | Python | https://github.com/httpie/fractional-indexing-python |
56
+
57
+ The code was ported entirely by ChatGPT.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/test_*.rb"]
10
+ end
11
+
12
+ task default: :test
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FractionalIndexing
4
+ VERSION = "0.1.1"
5
+ end
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Provides functions for generating ordering strings
4
+ # <https://observablehq.com/@dgreensp/implementing-fractional-indexing>
5
+ # <https://github.com/aalin/fractional_indexing.rb>
6
+ # Ported from Python using ChatGPT:
7
+ # <https://github.com/httpie/fractional-indexing-python>
8
+
9
+ require_relative "fractional_indexing/version"
10
+ require "bigdecimal"
11
+
12
+ module FractionalIndexing
13
+ class Error < StandardError
14
+ end
15
+
16
+ BASE_62_DIGITS =
17
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
18
+ SMALLEST_INTEGER = "A00000000000000000000000000"
19
+ INTEGER_ZERO = "a0"
20
+
21
+ def self.midpoint(a, b, digits)
22
+ # `a` may be empty string, `b` is null or non-empty string.
23
+ # `a < b` lexicographically if `b` is non-null.
24
+ # no trailing zeros allowed.
25
+ # digits is a string such as '0123456789' for base 10. Digits must be in
26
+ # ascending character code order!
27
+ raise Error, "#{a} >= #{b}" if b && a >= b
28
+ raise Error, "trailing zero" if (a[-1] == "0") || (b && (b[-1] == "0"))
29
+ if b
30
+ # remove longest common prefix. pad `a` with 0s as we
31
+ # go. note that we don't need to pad `b`, because it can't
32
+ # end before `a` while traversing the common prefix.
33
+ n = 0
34
+ a = a.ljust(b.length, "0")
35
+ for i in 0..(b.length - 1)
36
+ if a[i] == b[i]
37
+ n += 1
38
+ else
39
+ break
40
+ end
41
+ end
42
+
43
+ return b[0..(n - 1)] + midpoint(a[n..-1], b[n..-1], digits) if n > 0
44
+ end
45
+
46
+ # first digits (or lack of digit) are different
47
+ digit_a = a.empty? ? 0 : digits.index(a[0])
48
+ digit_b = b.nil? ? digits.length : digits.index(b[0])
49
+ if digit_b - digit_a > 1
50
+ min_digit = (0.5 * (digit_a + digit_b)).round
51
+ return digits[min_digit]
52
+ else
53
+ if b && b.length > 1
54
+ return b[0]
55
+ else
56
+ # `b` is null or has length 1 (a single digit).
57
+ # the first digit of `a` is the previous digit to `b`,
58
+ # or 9 if `b` is null.
59
+ # given, for example, midpoint('49', '5'), return
60
+ # '4' + midpoint('9', null), which will become
61
+ # '4' + '9' + midpoint('', null), which is '495'
62
+ digit_a = a.empty? ? 0 : digits.index(a[0])
63
+ return digits[digit_a] + midpoint(a[1..-1], nil, digits)
64
+ end
65
+ end
66
+ end
67
+
68
+ def self.validate_integer(i)
69
+ unless i.length == get_integer_length(i[0])
70
+ raise Error, "invalid integer part of order key: #{i}"
71
+ end
72
+ end
73
+
74
+ def self.get_integer_length(head)
75
+ if ('a'..'z').cover?(head)
76
+ return head.ord - 'a'.ord + 2
77
+ elsif ('A'..'Z').cover?(head)
78
+ return 'Z'.ord - head.ord + 2
79
+ end
80
+ raise Error, "invalid order key head: " + head
81
+ end
82
+
83
+ def self.get_integer_part(key)
84
+ integer_part_length = get_integer_length(key[0])
85
+ raise Error, "invalid order key: #{key}" if integer_part_length > key.size
86
+
87
+ key[0...integer_part_length]
88
+ end
89
+
90
+ def self.validate_order_key(key)
91
+ raise Error, "invalid order key: #{key}" if key == SMALLEST_INTEGER
92
+
93
+ # get_integer_part() will throw if the first character is bad,
94
+ # or the key is too short. we'd call it to check these things
95
+ # even if we didn't need the result
96
+ i = get_integer_part(key)
97
+ f = key[i.size..]
98
+ raise Error, "invalid order key: #{key}" if f[-1] == "0"
99
+
100
+ nil
101
+ end
102
+
103
+ def self.increment_integer(x, digits)
104
+ validate_integer(x)
105
+ head, *digs = x.chars
106
+ carry = true
107
+ digs.reverse_each do |d|
108
+ i = digits.index(d) + 1
109
+ if i == digits.size
110
+ d.replace('0')
111
+ else
112
+ d.replace(digits[i])
113
+ carry = false
114
+ break
115
+ end
116
+ end
117
+ if carry
118
+ if head == 'Z'
119
+ return 'a0'
120
+ elsif head == 'z'
121
+ return nil
122
+ end
123
+ h = (head.ord + 1).chr
124
+ if h > 'a'
125
+ digs.push('0')
126
+ else
127
+ digs.pop
128
+ end
129
+ return h + digs.join
130
+ else
131
+ return head + digs.join
132
+ end
133
+ end
134
+
135
+ def self.decrement_integer(x, digits)
136
+ validate_integer(x)
137
+ head, *digs = x.chars
138
+ borrow = true
139
+ digs.reverse_each do |d|
140
+ i = digits.index(d) - 1
141
+ if i == -1
142
+ d.replace(digits[-1])
143
+ else
144
+ d.replace(digits[i])
145
+ borrow = false
146
+ break
147
+ end
148
+ end
149
+ if borrow
150
+ if head == 'a'
151
+ return 'Z' + digits[-1]
152
+ elsif head == 'A'
153
+ return nil
154
+ end
155
+ h = (head.ord - 1).chr
156
+ if h < 'Z'
157
+ digs.push(digits[-1])
158
+ else
159
+ digs.pop
160
+ end
161
+ return h + digs.join
162
+ else
163
+ return head + digs.join
164
+ end
165
+ end
166
+
167
+ def self.generate_key_between(a, b, digits = BASE_62_DIGITS)
168
+ validate_order_key(a) if a
169
+ validate_order_key(b) if b
170
+ raise "a >= b" if a && b && a >= b
171
+
172
+ if a == nil
173
+ return INTEGER_ZERO if b == nil
174
+ ib = get_integer_part(b)
175
+ fb = b[ib.length..-1]
176
+ return ib + midpoint("", fb, digits) if ib == SMALLEST_INTEGER
177
+ return ib if ib < b
178
+ res = decrement_integer(ib, digits)
179
+ raise "cannot decrement any more" if res == nil
180
+ return res
181
+ end
182
+
183
+ if b == nil
184
+ ia = get_integer_part(a)
185
+ fa = a[ia.length..-1]
186
+ i = increment_integer(ia, digits)
187
+ return ia + midpoint(fa, nil, digits) if i == nil
188
+ return i
189
+ end
190
+
191
+ ia = get_integer_part(a)
192
+ fa = a[ia.length..-1]
193
+ ib = get_integer_part(b)
194
+ fb = b[ib.length..-1]
195
+ return ia + midpoint(fa, fb, digits) if ia == ib
196
+ i = increment_integer(ia, digits)
197
+ raise "cannot increment any more" if i == nil
198
+
199
+ return i if i < b
200
+
201
+ return ia + midpoint(fa, nil, digits)
202
+ end
203
+
204
+ # Returns an array of n distinct keys in sorted order.
205
+ # If a and b are both nil, returns [a0, a1, ...]
206
+ # If one or the other is nil, returns consecutive "integer"
207
+ # keys. Otherwise, returns relatively short keys between.
208
+ def self.generate_n_keys_between(a, b, n, digits = BASE_62_DIGITS)
209
+ return [] if n.zero?
210
+
211
+ return [generate_key_between(a, b, digits)] if n == 1
212
+
213
+ unless b
214
+ c = generate_key_between(a, b, digits)
215
+ result = [c]
216
+ (n - 1).times do
217
+ c = generate_key_between(c, b, digits)
218
+ result << c
219
+ end
220
+ return result
221
+ end
222
+
223
+ unless a
224
+ c = generate_key_between(a, b, digits)
225
+ result = [c]
226
+ (n - 1).times do
227
+ c = generate_key_between(a, c, digits)
228
+ result << c
229
+ end
230
+ return result.reverse
231
+ end
232
+
233
+ mid = n / 2
234
+ c = generate_key_between(a, b, digits)
235
+ [
236
+ *generate_n_keys_between(a, c, mid.floor, digits),
237
+ c,
238
+ *generate_n_keys_between(c, b, n - mid.floor - 1, digits)
239
+ ]
240
+ end
241
+
242
+ # Rounds a float to an integer using decimal.Decimal.quantize with
243
+ # decimal.ROUND_HALF_UP rounding method.
244
+ def self.round_half_up(n)
245
+ (n.to_d.round(0, half: :up)).to_i
246
+ end
247
+ end
@@ -0,0 +1,4 @@
1
+ module FractionalIndexing
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fractional_indexing
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Andreas Alin
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-04-07 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - andreas.alin@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - Gemfile
21
+ - Gemfile.lock
22
+ - LICENSE
23
+ - README.md
24
+ - Rakefile
25
+ - lib/fractional_indexing.rb
26
+ - lib/fractional_indexing/version.rb
27
+ - sig/fractional_indexing.rbs
28
+ homepage: https://github.com/aalin/fractional_indexing.rb
29
+ licenses:
30
+ - " CC0-1.0"
31
+ metadata:
32
+ homepage_uri: https://github.com/aalin/fractional_indexing.rb
33
+ source_code_uri: https://github.com/aalin/fractional_indexing.rb
34
+ post_install_message:
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 2.6.0
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirements: []
49
+ rubygems_version: 3.4.6
50
+ signing_key:
51
+ specification_version: 4
52
+ summary: Fractional Indexing in Ruby
53
+ test_files: []