With that, you could pick ideal points between bounds very cheaply:
(1/4, 1/2) → 1/3
(4/3, 5/2) → 2/1
(5/4, 7/5) → 4/3
In languages with large integers, it's quite easy to store two numbers. In a language like JavaScript, 53 bits of the mantissa in a double floating point number give 70+ moves [1] before you'd have to worry about losing precision, and if that were a deal breaker, a continued fraction is very cheap to store instead.If people are interested, I'd be happy to share some implementations I've got in a number of languages including Erlang and JavaScript (I'm rewriting these from a prior version I did which used a slightly less optimal path representation).
[1] These are worst case. Most interactions could survive 1000+ resorts in practice. I've yet to see any of the components outgrow the 53bit mantissa of a double in JavaScript. The worse case grows with the Fibonacci sequence which is easy to see once you learn more about the Stern-Brocot tree structure. Even when they do grow large though, it's easy to switch to the continued fraction representation which solves this problem.
EDIT: Here is a short list of indices from a production database where it's easy to see why fractions are far superior to floating point indices: https://gist.github.com/strmpnk/d4afe4bb5bb69b46631b
Another solution I made myself is just to use string as a key. If you need to insert something between "a" and "b", it'll become "an". If you need to insert something between "an" and "am", it'll become "ann", etc. It uses 26 letters but probably should use 64 different characters. This way you'll manipulate strings, they could be easily sorted and you'll figure out next string quite fast. But they could grow long, so entire recalculation should be done from time to time.
This is handy though. Consider having something at index 'a'. Now find something that comes right before it. You can't if you use traditional alphabetical ordering.
The above might seem odd but is much more reliable than midpoints and not hard to implement. The representation of the index can use multiple formats, one of the easiest is just a pair of numbers in a fraction.
The problem with floating point numbers is that you'lol lose precision faster than you might expect. See my gist for an example of 40 fractions from a production database where there is no double precision floating point number between them.
"you'd need a library" is not a solution when you have API consumers in dozens of languages.
Seriously, I've done both of these and the complexity is about the same. One route just ends up with a much better separation of concerns where the other has surprising edge cases which I see hit far more often than I would have imagined.
When I first wrote it, I started with midpoints but it doesn't take long to see that it gets messy to optimize. I eventually stumbled on the b-tree representation of all positive rational numbers (Stern-Brocot) and the optimization became obvious.
Adding logic to pick 1 instead of 1.25 seems easy but there are a lot of edge cases and ends up creating something that is at least as complex as this simple binary tree that generates all positive rational numbers.
Came up with something like this but wanted to avoid ever having to update the entire list. A combo of Rubaxa Sortable's 'onEnd' option and Vue to render card properties in HTML attributes make it easy.
Finally, if two cards end up with values too close together (just some magic number - .0001 apart or something) then those cards and some nearby are all re-numbered.
This means that most of the time you only have to update a single card. In degenerate movement cases, you might update up to an entire list, but that can only happen every so many moves (~20 or so, but depends on exact values) even in the worst case.