fractional_indexer 0.3.1 → 0.4.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d3588d2624cbc372dd3013729e5543eeabf9ab227005bf5c7a02e59360d7dcb
4
- data.tar.gz: 1b11cf313ea2e71ed55bab5f394fcab8d1797e08b7a86df7012bcfafe43d0f68
3
+ metadata.gz: 647cdf5b4ac5ad3835667390221cfb694a37be44b50f494326ecc1a4d5a75b75
4
+ data.tar.gz: 3496c2e7f1132bb5699a26d614e9a66999733caeddd4b4d0c463cdcf2977848b
5
5
  SHA512:
6
- metadata.gz: d8735e7f53ea599b2a3cbfdaafd34cd2c854991c461d02b4383a7a47fb36ab956d9bbdbd80c0cf018ab668050133e0039c4e117e654cba77da2ee9fe964b70b9
7
- data.tar.gz: 6b4c53291cb4fb77ebb3fb279531693a8b2c6c74dd1ad775e6bb69d19bd2e23d742a3316c9922df94dcaad24a3af0c260b14aa3caacf4b7f142844f1fb9537bc
6
+ metadata.gz: fdac9f7b6ebee39cdf765b81d5cb5c739632e1ad0b7de48e1b28f465a60a0b0afbdf1157ffe5df3f9ebddddca0d5b7d6c96323b5d80424e138ee36abcc1624da
7
+ data.tar.gz: 28bd39cf30678fed311bf1b9908ac76824b079859fa755bd8c2b181147ef3646dccbca8f257ee3ac763f0d2cff3f9f54b64bc398243d6ddcf7a3539a11da2155
data/README.md CHANGED
@@ -3,13 +3,45 @@
3
3
  [![codecov](https://codecov.io/gh/kazu-2020/fractional_indexer/graph/badge.svg?token=OCCYE4EKT1)](https://codecov.io/gh/kazu-2020/fractional_indexer)
4
4
  [![test](https://github.com/kazu-2020/fractional_indexer/actions/workflows/ruby.yml/badge.svg?branch=main&event=push)](https://github.com/kazu-2020/fractional_indexer/actions/workflows/ruby.yml)
5
5
 
6
- > efficient data insertion and sorting through fractional indexing
6
+ > Efficient data insertion and sorting through fractional indexing
7
7
 
8
- This gem is designed to achieve "[Realtime editing of ordered sequences](https://www.figma.com/blog/realtime-editing-of-ordered-sequences/#fractional-indexing)" as featured in a Figma blog post.
8
+ ## Overview
9
9
 
10
- Specifically, it implements Fractional Indexing as a means of managing order, realized through strings. By managing indexes as strings, it significantly delays the occurrence of index duplication that can arise when implementing Fractional Indexing with floating-point numbers. Additionally, using strings allows for the representation of a much larger range of numbers in a single digit compared to decimal numbers (by default, a single digit is represented in base-62).
10
+ Fractional Indexer is a Ruby gem that implements **fractional indexing** for managing ordered sequences. Instead of using integer positions that require reindexing on insertion, it uses string-based keys that allow inserting items anywhere without affecting existing items.
11
11
 
12
- This gem was implemented based on the excellent article "[Implementing Fractional Indexing.](https://observablehq.com/@dgreensp/implementing-fractional-indexing)" I would like to express my gratitude to David Greenspan, the author of the article.
12
+ ### Why Fractional Indexing?
13
+
14
+ **Traditional integer indexing** requires shifting all subsequent items when inserting:
15
+
16
+ ```
17
+ Before: [A:1] [B:2] [C:3]
18
+
19
+ Insert X between A and B
20
+
21
+ After: [A:1] [X:2] [B:3] [C:4] ← B and C must be updated!
22
+ ```
23
+
24
+ **Fractional indexing** generates a key between existing keys without reindexing:
25
+
26
+ ```
27
+ Before: [A:"a0"] [B:"a1"] [C:"a2"]
28
+
29
+ Insert X between A and B
30
+
31
+ After: [A:"a0"] [X:"a0V"] [B:"a1"] [C:"a2"] ← No changes to B or C!
32
+ ```
33
+
34
+ ### Key Features
35
+
36
+ - **No reindexing required** - Insert items between any two existing items
37
+ - **String-based keys** - Avoids floating-point precision issues
38
+ - **Configurable base** - Supports base-10, base-62 (default), and base-94
39
+ - **Multiple key generation** - Generate multiple keys at once for batch operations
40
+
41
+ This gem implements the concepts from "[Realtime editing of ordered sequences](https://www.figma.com/blog/realtime-editing-of-ordered-sequences/#fractional-indexing)" (Figma Engineering Blog).
42
+
43
+ > [!TIP]
44
+ > **Using Rails?** Check out [narabikae](https://github.com/kazu-2020/narabikae) - an Active Record integration that makes fractional indexing as simple as `task.move_to_position_after(other_task)`
13
45
 
14
46
  ## Installation
15
47
 
@@ -21,100 +53,310 @@ gem 'fractional_indexer'
21
53
 
22
54
  And then execute:
23
55
 
24
- $ bundle
56
+ ```sh
57
+ bundle
58
+ ```
25
59
 
26
60
  Or install it yourself as:
27
61
 
28
- $ gem install fractional_indexer
62
+ ```sh
63
+ gem install fractional_indexer
64
+ ```
65
+
66
+ ## Quick Start
29
67
 
30
- ## Usage
68
+ ```ruby
69
+ require 'fractional_indexer'
70
+
71
+ # Step 1: Generate your first key
72
+ first_key = FractionalIndexer.generate_key
73
+ # => "a0"
74
+
75
+ # Step 2: Generate the next key (for appending)
76
+ second_key = FractionalIndexer.generate_key(prev_key: first_key)
77
+ # => "a1"
31
78
 
32
- ### Generate Order key
79
+ # Step 3: Insert between two keys
80
+ middle_key = FractionalIndexer.generate_key(prev_key: first_key, next_key: second_key)
81
+ # => "a0V"
33
82
 
34
- To generate an order key, use the `FractionalIndexer.generate_key` method. This method can take two arguments: prev_key and next_key, both of which can be either nil or a string (excluding empty strings).
83
+ # Result: first_key < middle_key < second_key
84
+ # "a0" < "a0V" < "a1"
85
+ ```
86
+
87
+ ## How It Works
88
+
89
+ ### Order Key Structure
90
+
91
+ An order key consists of two parts: an **integer part** and an optional **fractional part**.
92
+
93
+ ```
94
+ "a3012"
95
+ │└┬┘└┬┘
96
+ │ │ └── Fractional Part: "012" (optional, for fine-grained positioning)
97
+ │ └───── Integer Digits: "3" (the numeric value)
98
+ └─────── Prefix: "a" (indicates 1-digit positive integer)
99
+ ```
100
+
101
+ **Prefix rules:**
102
+ - `a` to `z`: Positive integers (a=1 digit, b=2 digits, ..., z=26 digits)
103
+ - `A` to `Z`: Negative integers (used for keys "before" zero)
104
+
105
+ **Examples:**
106
+
107
+ | Key | Integer Part | Fractional Part | Meaning |
108
+ |-----|-------------|-----------------|---------|
109
+ | `a5` | `a5` | (none) | Positive 1-digit: 5 |
110
+ | `b12` | `b12` | (none) | Positive 2-digit: 12 |
111
+ | `a3V` | `a3` | `V` | Between a3 and a4 |
112
+ | `Zz` | `Zz` | (none) | Largest negative number |
113
+
114
+ ### Key Generation Flow
35
115
 
36
- The characters that can be used in the strings are available for review in `FractionalIndexer.configuration.digits`.
116
+ The following diagram shows how `generate_key` determines which operation to perform:
117
+
118
+ ```mermaid
119
+ flowchart TD
120
+ A[generate_key] --> B{prev_key and next_key?}
121
+ B -->|Both nil| C["Return 'a0'<br/>(initial key)"]
122
+ B -->|Only prev_key| D["Increment<br/>(next key after prev)"]
123
+ B -->|Only next_key| E["Decrement<br/>(key before next)"]
124
+ B -->|Both provided| F["Midpoint<br/>(key between both)"]
125
+
126
+ D --> G["a0 → a1 → a2 → ..."]
127
+ E --> H["... → Zy → Zz → a0"]
128
+ F --> I["a0, a2 → a1<br/>a0, a1 → a0V"]
129
+ ```
130
+
131
+ ## Usage
132
+
133
+ ### Basic Usage
134
+
135
+ #### Generating a Single Key
37
136
 
38
137
  ```ruby
39
138
  require 'fractional_indexer'
40
139
 
41
- # create first order key
140
+ # Create the first order key (when no keys exist)
42
141
  FractionalIndexer.generate_key
43
142
  # => "a0"
44
143
 
45
- # increment
144
+ # Increment: generate key after a given key
46
145
  FractionalIndexer.generate_key(prev_key: 'a0')
47
146
  # => "a1"
48
147
 
49
- # decrement
148
+ # Decrement: generate key before a given key
50
149
  FractionalIndexer.generate_key(next_key: 'a0')
51
150
  # => "Zz"
52
151
 
53
- # between prev_key and next_key
152
+ # Between: generate key between two keys
54
153
  FractionalIndexer.generate_key(prev_key: 'a0', next_key: 'a2')
55
154
  # => "a1"
56
-
57
- # prev_key should be less than next_key
58
- FractionalIndexer.generate_key(prev_key: 'a2', next_key: 'a1')
59
- # => error
60
- FractionalIndexer.generate_key(prev_key: 'a1', next_key: 'a1')
61
- # => error
62
155
  ```
63
156
 
64
- ### Generate Multiple Order keys
65
-
66
- To generate multiple order keys, use the `FractionalIndexer.generate_keys` method.
157
+ #### Generating Multiple Keys
67
158
 
68
159
  ```ruby
69
- require 'fractional_indexer'
70
-
71
- # generate n order keys that sequentially follow a given prev_key.
160
+ # Generate 5 keys after "b11"
72
161
  FractionalIndexer.generate_keys(prev_key: "b11", count: 5)
73
162
  # => ["b12", "b13", "b14", "b15", "b16"]
74
163
 
75
- # generate n order keys that sequentially precede a given next_key.
164
+ # Generate 5 keys before "b11"
76
165
  FractionalIndexer.generate_keys(next_key: "b11", count: 5)
77
166
  # => ["b0w", "b0x", "b0y", "b0z", "b10"]
78
167
 
79
- # generate n order keys between the given prev_key and next_key.
168
+ # Generate 5 keys between "b10" and "b11"
80
169
  FractionalIndexer.generate_keys(prev_key: "b10", next_key: "b11", count: 5)
81
170
  # => ["b108", "b10G", "b10V", "b10d", "b10l"]
82
171
  ```
83
172
 
84
- ## Configure
173
+ #### Error Handling
174
+
175
+ ```ruby
176
+ # prev_key must be less than next_key
177
+ FractionalIndexer.generate_key(prev_key: 'a2', next_key: 'a1')
178
+ # => raises error
179
+
180
+ # prev_key and next_key cannot be equal
181
+ FractionalIndexer.generate_key(prev_key: 'a1', next_key: 'a1')
182
+ # => raises error
183
+ ```
184
+
185
+ ### Practical Examples
186
+
187
+ #### Example 1: Task List Management
188
+
189
+ ```ruby
190
+ # Managing a todo list with fractional indexing
191
+ tasks = []
192
+
193
+ # Add initial tasks
194
+ tasks << { id: 1, title: "Write code", position: FractionalIndexer.generate_key }
195
+ tasks << { id: 2, title: "Write tests", position: FractionalIndexer.generate_key(prev_key: tasks.last[:position]) }
196
+ tasks << { id: 3, title: "Deploy", position: FractionalIndexer.generate_key(prev_key: tasks.last[:position]) }
197
+
198
+ tasks.each { |t| puts "#{t[:position]}: #{t[:title]}" }
199
+ # a0: Write code
200
+ # a1: Write tests
201
+ # a2: Deploy
202
+
203
+ # Insert "Code review" between "Write tests" and "Deploy"
204
+ new_position = FractionalIndexer.generate_key(
205
+ prev_key: tasks[1][:position], # "a1"
206
+ next_key: tasks[2][:position] # "a2"
207
+ )
208
+ tasks << { id: 4, title: "Code review", position: new_position }
209
+
210
+ # Sort by position
211
+ tasks.sort_by! { |t| t[:position] }
212
+ tasks.each { |t| puts "#{t[:position]}: #{t[:title]}" }
213
+ # a0: Write code
214
+ # a1: Write tests
215
+ # a1V: Code review ← Inserted without changing other positions!
216
+ # a2: Deploy
217
+ ```
218
+
219
+ #### Example 2: Prepending and Appending
220
+
221
+ ```ruby
222
+ # Start with a middle item
223
+ items = [{ name: "B", pos: FractionalIndexer.generate_key }]
224
+ # items[0][:pos] => "a0"
225
+
226
+ # Append to the end (only prev_key)
227
+ items << { name: "C", pos: FractionalIndexer.generate_key(prev_key: items.last[:pos]) }
228
+ # items[1][:pos] => "a1"
229
+
230
+ # Prepend to the beginning (only next_key)
231
+ items.unshift({ name: "A", pos: FractionalIndexer.generate_key(next_key: items.first[:pos]) })
232
+ # items[0][:pos] => "Zz"
233
+
234
+ items.sort_by { |i| i[:pos] }.each { |i| puts "#{i[:pos]}: #{i[:name]}" }
235
+ # Zz: A
236
+ # a0: B
237
+ # a1: C
238
+ ```
239
+
240
+ #### Example 3: Batch Insertion
241
+
242
+ ```ruby
243
+ # Insert 5 items between two existing items at once
244
+ existing = [
245
+ { name: "First", pos: "a0" },
246
+ { name: "Last", pos: "a1" }
247
+ ]
248
+
249
+ # Generate 5 keys between "a0" and "a1"
250
+ new_positions = FractionalIndexer.generate_keys(
251
+ prev_key: existing[0][:pos],
252
+ next_key: existing[1][:pos],
253
+ count: 5
254
+ )
255
+ # => ["a08", "a0G", "a0V", "a0d", "a0l"]
256
+
257
+ new_items = new_positions.map.with_index do |pos, i|
258
+ { name: "Item #{i + 1}", pos: pos }
259
+ end
260
+
261
+ all_items = (existing + new_items).sort_by { |i| i[:pos] }
262
+ all_items.each { |i| puts "#{i[:pos]}: #{i[:name]}" }
263
+ # a0: First
264
+ # a08: Item 1
265
+ # a0G: Item 2
266
+ # a0V: Item 3
267
+ # a0d: Item 4
268
+ # a0l: Item 5
269
+ # a1: Last
270
+ ```
271
+
272
+ #### Example 4: Key Growth Over Time
273
+
274
+ When repeatedly inserting at the same position, keys grow longer to maintain precision:
85
275
 
86
- ### base
276
+ ```ruby
277
+ # Repeatedly insert at the beginning
278
+ key = FractionalIndexer.generate_key # => "a0"
279
+ puts "Initial: #{key}"
87
280
 
88
- You can set the base (number system) used to represent each digit. The possible values are :base_10, :base_62, and :base_94. (The default is :base_62.)
281
+ 5.times do |i|
282
+ key = FractionalIndexer.generate_key(prev_key: key, next_key: "a1")
283
+ puts "Insert #{i + 1}: #{key}"
284
+ end
89
285
 
90
- Note: base_10 is primarily intended for operational verification and debugging purposes.
286
+ # Initial: a0
287
+ # Insert 1: a0V
288
+ # Insert 2: a0l
289
+ # Insert 3: a0t
290
+ # Insert 4: a0x
291
+ # Insert 5: a0z
292
+ ```
293
+
294
+ ## Configuration
295
+
296
+ ### Base System
297
+
298
+ You can configure the base (number system) used to represent each digit. The possible values are `:base_10`, `:base_62` (default), and `:base_94`.
299
+
300
+ | Base | Characters | Use Case |
301
+ |------|-----------|----------|
302
+ | `:base_10` | `0-9` | Debugging, human-readable |
303
+ | `:base_62` | `0-9`, `A-Z`, `a-z` | General use (default) |
304
+ | `:base_94` | All printable ASCII | Maximum density |
91
305
 
92
306
  ```ruby
93
307
  require 'fractional_indexer'
94
308
 
309
+ # Base 10 (for debugging)
95
310
  FractionalIndexer.configure do |config|
96
311
  config.base = :base_10
97
312
  end
98
313
  FractionalIndexer.configuration.digits.join
99
314
  # => "0123456789"
100
315
 
316
+ # Base 62 (default)
101
317
  FractionalIndexer.configure do |config|
102
318
  config.base = :base_62
103
319
  end
104
320
  FractionalIndexer.configuration.digits.join
105
321
  # => "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
106
322
 
323
+ # Base 94 (maximum density)
107
324
  FractionalIndexer.configure do |config|
108
325
  config.base = :base_94
109
326
  end
110
327
  FractionalIndexer.configuration.digits.join
111
- # => "!\"\#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
328
+ # => "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
329
+ ```
330
+
331
+ ## Related Projects
332
+
333
+ ### narabikae
334
+
335
+ If you're using **Ruby on Rails** with **Active Record**, check out [narabikae](https://github.com/kazu-2020/narabikae) - a gem that integrates Fractional Indexer directly into your models for seamless ordering.
336
+
337
+ ```ruby
338
+ class Task < ApplicationRecord
339
+ narabikae :position, size: 200
340
+ end
341
+
342
+ # Move a task after another
343
+ task.move_to_position_after(other_task)
344
+
345
+ # Move a task before another
346
+ task.move_to_position_before(other_task)
347
+
348
+ # Move a task between two others
349
+ task.move_to_position_between(task_a, task_b)
112
350
  ```
113
351
 
114
352
  ## Contributing
115
353
 
116
- Bug reports and pull requests are welcome on GitHub at https://github.com/kazu-2020/fractional_indexer.
354
+ Bug reports and pull requests are welcome on GitHub at <https://github.com/kazu-2020/fractional_indexer>.
117
355
 
118
356
  ## License
119
357
 
120
358
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
359
+
360
+ ## Acknowledgments
361
+
362
+ This gem was implemented based on the excellent article "[Implementing Fractional Indexing](https://observablehq.com/@dgreensp/implementing-fractional-indexing)" by David Greenspan. Thank you for the clear explanation and reference implementation!
@@ -3,8 +3,8 @@
3
3
  module FractionalIndexer
4
4
  class OrderKey
5
5
  INTEGER_BASE_DIGIT = 2
6
- POSITIVE_SIGNS = ("a".."z").freeze
7
- NEGATIVE_SIGNS = ("A".."Z").freeze
6
+ POSITIVE_SIGNS = ("a".."z")
7
+ NEGATIVE_SIGNS = ("A".."Z")
8
8
 
9
9
  attr_reader :key
10
10
 
@@ -58,6 +58,10 @@ module FractionalIndexer
58
58
  integer == minimum_integer
59
59
  end
60
60
 
61
+ def minimum?
62
+ minimum_integer? && fractional.empty?
63
+ end
64
+
61
65
  def present?
62
66
  !(key.nil? || key.empty?)
63
67
  end
@@ -78,14 +82,14 @@ module FractionalIndexer
78
82
  "z#{digits.last * POSITIVE_SIGNS.count}"
79
83
  end
80
84
 
81
- def raise_error(description = nil)
82
- raise Error, "invalid order key: '#{key}' description: #{description}"
83
- end
84
-
85
85
  def minimum_integer
86
86
  "A#{digits.first * POSITIVE_SIGNS.count}"
87
87
  end
88
88
 
89
+ def raise_error(description = nil)
90
+ raise Error, "invalid order key: '#{key}' description: #{description}"
91
+ end
92
+
89
93
  def valid_fractional?(fractional)
90
94
  !fractional.end_with?(digits.first)
91
95
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FractionalIndexer
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.1"
5
5
  end
@@ -91,9 +91,20 @@ module FractionalIndexer
91
91
  end
92
92
 
93
93
  def self.decrement(order_key)
94
+ # NOTE: edge case
95
+ # If an Order key does not have a fractional part and its integer part is equal to
96
+ # the minimum value (for example, in base_62: 'A00000000000000000000000000'),
97
+ # further decrement operations become impossible and will fail.
98
+ # However, this situation is typically unlikely to occur.
99
+ if order_key.minimum?
100
+ raise Error, "order key not decremented: #{order_key} is the minimum value"
101
+ end
94
102
  return order_key.integer + Midpointer.execute("", order_key.fractional) if order_key.minimum_integer?
95
103
 
96
- order_key.integer < order_key.key ? order_key.integer : order_key.decrement
104
+ decremented_order_key = OrderKey.new(order_key.integer < order_key.key ? order_key.integer : order_key.decrement)
105
+ return decremented_order_key.integer + Midpointer.execute if decremented_order_key.minimum?
106
+
107
+ decremented_order_key.key
97
108
  end
98
109
 
99
110
  def self.increment(order_key)
metadata CHANGED
@@ -1,20 +1,19 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fractional_indexer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
- - kazu
8
- autorequire:
9
- bindir: exe
7
+ - matazou
8
+ bindir: bin
10
9
  cert_chain: []
11
- date: 2024-03-07 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies: []
13
12
  description: |
14
13
  FractionalIndexer is a Ruby gem designed to leverage fractional indexing for ordering within lists or data structures.
15
14
  it enables efficient sorting and insertion of large volumes of data.
16
15
  email:
17
- - matazou@gmail.com
16
+ - 64774307+kazu-2020@users.noreply.github.com
18
17
  executables: []
19
18
  extensions: []
20
19
  extra_rdoc_files: []
@@ -32,9 +31,12 @@ homepage: https://github.com/kazu-2020/fractional_indexer
32
31
  licenses:
33
32
  - MIT
34
33
  metadata:
34
+ homepage_uri: https://github.com/kazu-2020/fractional_indexer
35
+ changelog_uri: https://github.com/kazu-2020/fractional_indexer/releases
36
+ bug_tracker_uri: https://github.com/kazu-2020/fractional_indexer/issues
35
37
  source_code_uri: https://github.com/kazu-2020/fractional_indexer
36
- changelog_uri: https://github.com/kazu-2020/fractional_indexer
37
- post_install_message:
38
+ allowed_push_host: https://rubygems.org/
39
+ rubygems_mfa_required: 'true'
38
40
  rdoc_options: []
39
41
  require_paths:
40
42
  - lib
@@ -42,15 +44,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
42
44
  requirements:
43
45
  - - ">="
44
46
  - !ruby/object:Gem::Version
45
- version: '0'
47
+ version: '3.1'
46
48
  required_rubygems_version: !ruby/object:Gem::Requirement
47
49
  requirements:
48
50
  - - ">="
49
51
  - !ruby/object:Gem::Version
50
52
  version: '0'
51
53
  requirements: []
52
- rubygems_version: 3.4.10
53
- signing_key:
54
+ rubygems_version: 3.6.7
54
55
  specification_version: 4
55
56
  summary: efficient data insertion and sorting through fractional indexing
56
57
  test_files: []