I have an array of Java-style package names and want to sort it in a specific way. The sort order I want is generally alphabetically, but with the additional requirement that lowercase always goes either before or after uppercase, depending on a user supplied flag to the sorting function (which allows also a third option of leaving the original order alone).
Example:
const importStrings = [
"foo.bar.Zonk",
"lorem.ipsum.acme.Rocket",
"foo.bar.BazFoo",
"lorem.ipsum.Blah",
"foo.bar.baz.Gnarf",
"lorem.ipsum.acme.Amboss",
];
The order I want is either uppercase first on any given .:
[
"foo.bar.BazFoo",
"foo.bar.Zonk",
"foo.bar.baz.Gnarf",
"lorem.ipsum.Blah",
"lorem.ipsum.acme.Amboss",
"lorem.ipsum.acme.Rocket",
]
or lowercase first:
[
"foo.bar.baz.Gnarf",
"foo.bar.BazFoo",
"foo.bar.Zonk",
"lorem.ipsum.acme.Amboss",
"lorem.ipsum.acme.Rocket",
"lorem.ipsum.Blah",
]
Effectively, I want the sort order to be A-Za-z/a-zA-Z instead of AaBb..Zz/aAbB..zZ, which seems to be surprisingly difficult to achieve unless I'm missing something really, really obvious. All my previous experiments with various custom Array.sort() functions and localeCompare() options didn't yield the output I need.
My current approach kinda-works, but contains a ton of boilerplate and cruft, and I'm thinking that there has to be a better way of doing this. I currently split the individual strings on ., recursively build a object graph containing each "level" of the package name in an array, and then sort that array. That gives me the ordering I want, which I can then merge back together:
function createImportData(data, parts, sortFunction, reduceFunction) {
const packageName = parts.shift();
let localData = data.find((obj) => obj.value === packageName);
if (!localData) {
localData = { value: packageName, children: [] };
data.push(localData);
}
if (parts.length) {
createImportData(localData.children, parts, sortFunction, reduceFunction);
localData.children = localData.children
.reduce(reduceFunction, [[], []])
.flatMap((array) => array.sort(sortFunction));
}
}
function getSortFunction(options) {
if (!shouldOrganizeImports(options)) {
// leave all
return (obj1, obj2) => 0;
} else {
return (obj1, obj2) => {
obj1.value.localeCompare(obj2.value);
};
}
}
function getReduceFunction(options) {
if (!shouldOrganizeImports(options)) {
// just put it in a single array without changing anything
return (groups, obj) => {
groups[0].push(obj);
return groups;
};
} else {
const upperIndex = shouldOrganizeImportsUppercaseFirst(options) ? 0 : 1;
const lowerIndex = shouldOrganizeImportsUppercaseFirst(options) ? 1 : 0;
const reduce = (upperIndex, lowerIndex) => (groups, obj) => {
obj.value.charAt(0).toUpperCase() === obj.value.charAt(0)
? groups[upperIndex].push(obj)
: groups[lowerIndex].push(obj);
return groups;
};
return reduce(upperIndex, lowerIndex);
}
}
//initial call looks like this:
const importData = [];
const sortFunction = getSortFunction(options);
const reduceFunction = getReduceFunction(options);
for (const importString of importStrings) {
createImportData(
importData,
importString.split("."),
sortFunction,
reduceFunction
);
}
The generated structure looks like this, with each level's children array sorted the way I need:
[
{
value: "foo",
children: [
{
value: "bar",
children: [
{ value: "baz", children: [{ value: "Gnarf", children: [] }] },
{ value: "BazFoo", children: [] },
{ value: "Zonk", children: [] },
],
},
],
},
{value: "lorem", children: [/* etc.etc. */]}
]
How can I improve on my logic, or is this already the way to do it?