I’m about to add new elements to existing and generate new .bike documents and was wondering about the id attributes attached to all the li
elements and the root ul
element. The generated id
s start short (of length 2) and then gradually grow in size. Naturally, the identifiers need to remain unique within the document. Anything else we should be aware of when we start adding our own id
s?
Here FWIW, expressed in JavaScript, is one working hypothesis:
Expand disclosure triangle to view JS source
(() => {
"use strict";
// ----------------- BIKE-STYLE IDS ------------------
// Rob Trew @ 2021
// Short, getting longer where necessary.
// Similar to those used in Jesse Grosjeans Bike.app
// characters are drawn from a 64 character URL-safe
// set:
// "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz-"
// bikeStyleID :: String -> Set String ->
// Int -> (IO Set String, IO String)
const bikeStyleID = charSet =>
charGen => seen => n => {
const go = sofar => m => {
const
f = () => Array.from({
length: m
}, () => charSet[charGen()])
.join(""),
limit = (charSet.length ** m) / 4;
return (limit > sofar.size) ? (() => {
const
newID = until(
k => !(sofar.has(k))
)(f)(f());
return Tuple(sofar.add(newID))(newID);
})() : Tuple(sofar)("");
};
// Larger keys returned if the set is becoming
// at all saturated at the starting length.
return until(
tpl => Boolean(tpl[1][1])
)(tpl => {
const next = 1 + tpl[0];
return Tuple(next)(go(tpl[1][0])(next));
})(
Tuple(n)(go(seen)(n))
)[1];
};
const main = () => {
const charSet = [
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ",
"_abcdefghijklmnopqrstuvwxyz-"
].join("");
const bikeCode = bikeStyleID(charSet)(
randomRInt(0)(charSet.length - 1)
);
const tuple = mapAccumL(
seen => () => bikeCode(seen)(2)
)(
new Set()
)(
enumFromTo(1)(10000)
);
return Array.from(tuple[0]);
};
// --------------------- GENERIC ---------------------
// Tuple (,) :: a -> b -> (a, b)
const Tuple = a =>
b => ({
type: "Tuple",
"0": a,
"1": b,
length: 2,
*[Symbol.iterator]() {
for (const k in this) {
if (!isNaN(k)) {
yield this[k];
}
}
}
});
// enumFromTo :: Int -> Int -> [Int]
const enumFromTo = m =>
n => Array.from({
length: 1 + n - m
}, (_, i) => m + i);
// mapAccumL :: (acc -> x -> (acc, y)) -> acc ->
// [x] -> (acc, [y])
const mapAccumL = f =>
// A tuple of an accumulation and a list
// obtained by a combined map and fold,
// with accumulation from left to right.
acc => xs => [...xs].reduce(
([a, bs], x) => second(
v => bs.concat(v)
)(
f(a)(x)
),
Tuple(acc)([])
);
// randomRInt :: Int -> Int -> IO () -> Int
const randomRInt = low =>
// The return value of randomRInt is itself
// a function, which, whenever evaluated,
// yields a a new pseudo-random integer
// in the range [low..high].
high => () => low + Math.floor(
Math.random() * (1 + (high - low))
);
// second :: (a -> b) -> ((c, a) -> (c, b))
const second = f =>
// A function over a simple value lifted
// to a function over a tuple.
// f (a, b) -> (a, f(b))
([x, y]) => Tuple(x)(f(y));
// until :: (a -> Bool) -> (a -> a) -> a -> a
const until = p =>
// The value resulting from repeated applications
// of f to the seed value x, terminating when
// that result returns true for the predicate p.
f => x => {
let v = x;
while (!p(v)) {
v = f(v);
}
return v;
};
return main();
})();
Unique and can be stored as xml attribute value are the big constraints. Also encodable in a URL, since these ids are used in Bike links. And if you construct a document and add non-unique ids they will be uniqueified on next load.
This generation style might change, you shouldn’t base any logic on it.
Also this logic is not followed for the root ul
id link. The root ID is always longer, and is intended to be “globally” unique, or at least machine unique. The root id is used in bike link urls so that you can connect from one Bike document to another.
It can create confusion if you have two documents with same root id, because the links to that id will resolve to two separate documents.