fractional_indexer 0.4.0 → 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: 9c3e15f1e2a102d07fb9fa39a650f28e959184a21bb347e8125df3c36437cfb5
4
- data.tar.gz: c83b4e10bcf12bed2604dcc2b230f481c0ad46ce7bd6f2777f3cb1fad3b836ad
3
+ metadata.gz: 647cdf5b4ac5ad3835667390221cfb694a37be44b50f494326ecc1a4d5a75b75
4
+ data.tar.gz: 3496c2e7f1132bb5699a26d614e9a66999733caeddd4b4d0c463cdcf2977848b
5
5
  SHA512:
6
- metadata.gz: ada9175bde75b29f21e7fe053cdcc0bb433c27c9fb9387de65d0b8816368f47a28108c410476b48f2bde9e5e4eb4855efe0aeef1fb0bce4291fd537a0403583b
7
- data.tar.gz: 82ee500b85f2dd7ad0883e3fbdf7028141e335e47010e47b9d305837dd45fd6c55d7f6d2e6f12ec8e83ad01cc4790a57238e4a37e82583ee1838c11cc86ef591
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
 
@@ -31,135 +63,292 @@ Or install it yourself as:
31
63
  gem install fractional_indexer
32
64
  ```
33
65
 
34
- ## Usage
66
+ ## Quick Start
67
+
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"
78
+
79
+ # Step 3: Insert between two keys
80
+ middle_key = FractionalIndexer.generate_key(prev_key: first_key, next_key: second_key)
81
+ # => "a0V"
82
+
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
- ### Generate Order key
116
+ The following diagram shows how `generate_key` determines which operation to perform:
37
117
 
38
- To generate an order key, use the `FractionalIndexer.generate_key` method.
39
- This method can take two arguments: prev_key and next_key, both of which can be either nil or a string (excluding empty strings).
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)"]
40
125
 
41
- The characters that can be used in the strings are available for review in `FractionalIndexer.configuration.digits`.
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
42
136
 
43
137
  ```ruby
44
138
  require 'fractional_indexer'
45
139
 
46
- # create first order key
140
+ # Create the first order key (when no keys exist)
47
141
  FractionalIndexer.generate_key
48
142
  # => "a0"
49
143
 
50
- # increment
144
+ # Increment: generate key after a given key
51
145
  FractionalIndexer.generate_key(prev_key: 'a0')
52
146
  # => "a1"
53
147
 
54
- # decrement
148
+ # Decrement: generate key before a given key
55
149
  FractionalIndexer.generate_key(next_key: 'a0')
56
150
  # => "Zz"
57
151
 
58
- # between prev_key and next_key
152
+ # Between: generate key between two keys
59
153
  FractionalIndexer.generate_key(prev_key: 'a0', next_key: 'a2')
60
154
  # => "a1"
61
-
62
- # prev_key should be less than next_key
63
- FractionalIndexer.generate_key(prev_key: 'a2', next_key: 'a1')
64
- # => error
65
- FractionalIndexer.generate_key(prev_key: 'a1', next_key: 'a1')
66
- # => error
67
155
  ```
68
156
 
69
- ### Generate Multiple Order keys
70
-
71
- To generate multiple order keys, use the `FractionalIndexer.generate_keys` method.
157
+ #### Generating Multiple Keys
72
158
 
73
159
  ```ruby
74
- require 'fractional_indexer'
75
-
76
- # generate n order keys that sequentially follow a given prev_key.
160
+ # Generate 5 keys after "b11"
77
161
  FractionalIndexer.generate_keys(prev_key: "b11", count: 5)
78
162
  # => ["b12", "b13", "b14", "b15", "b16"]
79
163
 
80
- # generate n order keys that sequentially precede a given next_key.
164
+ # Generate 5 keys before "b11"
81
165
  FractionalIndexer.generate_keys(next_key: "b11", count: 5)
82
166
  # => ["b0w", "b0x", "b0y", "b0z", "b10"]
83
167
 
84
- # generate n order keys between the given prev_key and next_key.
168
+ # Generate 5 keys between "b10" and "b11"
85
169
  FractionalIndexer.generate_keys(prev_key: "b10", next_key: "b11", count: 5)
86
170
  # => ["b108", "b10G", "b10V", "b10d", "b10l"]
87
171
  ```
88
172
 
89
- ## Configure
173
+ #### Error Handling
90
174
 
91
- ### base
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
92
220
 
93
- You can set the base (number system) used to represent each digit.
94
- The possible values are :base_10, :base_62, and :base_94. (The default is :base_62)
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
+ ```
95
239
 
96
- Note: base_10 is primarily intended for operational verification and debugging purposes.
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:
275
+
276
+ ```ruby
277
+ # Repeatedly insert at the beginning
278
+ key = FractionalIndexer.generate_key # => "a0"
279
+ puts "Initial: #{key}"
280
+
281
+ 5.times do |i|
282
+ key = FractionalIndexer.generate_key(prev_key: key, next_key: "a1")
283
+ puts "Insert #{i + 1}: #{key}"
284
+ end
285
+
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 |
97
305
 
98
306
  ```ruby
99
307
  require 'fractional_indexer'
100
308
 
309
+ # Base 10 (for debugging)
101
310
  FractionalIndexer.configure do |config|
102
311
  config.base = :base_10
103
312
  end
104
313
  FractionalIndexer.configuration.digits.join
105
314
  # => "0123456789"
106
315
 
316
+ # Base 62 (default)
107
317
  FractionalIndexer.configure do |config|
108
318
  config.base = :base_62
109
319
  end
110
320
  FractionalIndexer.configuration.digits.join
111
321
  # => "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
112
322
 
323
+ # Base 94 (maximum density)
113
324
  FractionalIndexer.configure do |config|
114
325
  config.base = :base_94
115
326
  end
116
327
  FractionalIndexer.configuration.digits.join
117
- # => "!\"\#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
328
+ # => "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
118
329
  ```
119
330
 
120
- ## Order key
121
-
122
- This section explains the structure of the string that represents an Order Key.
123
- An Order Key string is broadly divided into two parts: the integer part and the fractional part.
331
+ ## Related Projects
124
332
 
125
- Note: For ease of understanding, the explanation from here on will be based on the decimal system ('0' ~ '9')
333
+ ### narabikae
126
334
 
127
- ### Integer Part
128
-
129
- The integer part of the Order Key begins with a `mandatory prefix` that indicates the number of digits. This prefix is represented by one of the letters A to Z or a to z.
130
- a is 1 digit, b is 2 digits, c is 3 digits ...... and z represents 26 digits.
131
- The number of characters following the prefix must match the number of digits indicated by the prefix.
132
- For example, if the prefix is a, a valid key could be 'a8', and if the prefix is c, a valid key could be 'c135'.
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.
133
336
 
134
337
  ```ruby
135
- 'a9' # Valid
136
- 'b10' # Valid
137
- 'b9' # Invalid (The prefix 'b' requires two digits)
138
- 'c123' # Valid
139
- 'c12' # Invalid (The prefix 'c' requires three digits)
140
- ```
141
-
142
- Additionally, leveraging the characteristic that uppercase letters have a lower ASCII value than lowercase letters, a to z represent positive integers, while A to Z represent negative integers.
143
-
144
- ### Fractional Part
145
-
146
- The Fractional Part refers to the portion of the string that follows the Integer Part, excluding the Integer Part itself.
338
+ class Task < ApplicationRecord
339
+ narabikae :position, size: 200
340
+ end
147
341
 
148
- ```ruby
149
- 'a3012'
150
- # 'a3' : Integer Part
151
- # '012': Fractional Part
152
- ```
342
+ # Move a task after another
343
+ task.move_to_position_after(other_task)
153
344
 
154
- The Fractional Part cannot end with the first character of the base being used (e.g., '0' in base_10). This rule mirrors the mathematical principle that, for example, 0.3 and 0.30 represent the same value.
345
+ # Move a task before another
346
+ task.move_to_position_before(other_task)
155
347
 
156
- ```ruby
157
- 'a32' # Valid
158
- 'a320' # Invalid (The Fractional Part cannot end with '0' in base_10)
348
+ # Move a task between two others
349
+ task.move_to_position_between(task_a, task_b)
159
350
  ```
160
351
 
161
- This section clarifies the concept and rules regarding the Fractional Part of an Order Key, with examples to illustrate what constitutes a valid or invalid Fractional Part.
162
-
163
352
  ## Contributing
164
353
 
165
354
  Bug reports and pull requests are welcome on GitHub at <https://github.com/kazu-2020/fractional_indexer>.
@@ -167,3 +356,7 @@ Bug reports and pull requests are welcome on GitHub at <https://github.com/kazu-
167
356
  ## License
168
357
 
169
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FractionalIndexer
4
- VERSION = "0.4.0"
4
+ VERSION = "0.4.1"
5
5
  end
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.4.0
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-10 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: []