Custom sorting of text

I'm trying to do a macro that will sort some text according to some text. Can anyone provide a macro that will do below?

This is what I would like it to do step by step:

  1. Ask me to input the text to sort by (but with a default value already inserted). This will always be comma separated, for example "Common Nighthawk,Chimney Swift,Yellow-billed Cuckoo,Black-billed Cuckoo".

  2. Use text I have selected in some other app as input. Again this will be always be comma separated, for example "Black-billed Cuckoo,Chimney Swift,Yellow-billed Cuckoo,Common Nighthawk"

  3. Replace the selected text with the sorted text. In the above examples, the selected text "Black-billed Cuckoo,Chimney Swift,Yellow-billed Cuckoo,Common Nighthawk" will be replaced with "Common Nighthawk,Chimney Swift,Yellow-billed Cuckoo,Black-billed Cuckoo".

I'm not sure that the meaning of this is fully pinned down yet.

input the text
text I have selected in some other app as input

What is the expected overlap between pre-ordered list and the selected list ?

The selected list will (always? | sometimes?) form a subset of the pre-ordered list ?

What happens when one or more items in the selection are not found in the pre-ordered list ?

How close is the match between preordered items and selected items ? Case-sensitive ?

Yellow billed and Black billed will always be hyphenated ? Usually ?

etc

Should generally be overlap between pre-ordered list and selected list but in some cases none of pre-ordered list will be in selected list, and none of selected list will be in pre-ordered list

When items in pre-ordered list are not found in selected list, then still reorder items in selected list in the order they appear in pre-ordered list

pre-ordered list will match selected list including characters and cases

Yellow-billed Cuckoo and Black-billed Cuckoo always hyphenated

And where do you want selected items which don't figure in the pre-ordered listing ? Pushed to the front or to the end ?

Putting aside, for the moment, the IO:

  • capture or confirmation of custom sort order
  • copying of selecting CSV
  • re-pasting of sorted copy

If you can:

  1. bind your custom sort order CSV to the KM variable name local_PreOrderedCSV, and
  2. bind your selected CSV to local_CopiedCSV

then these draft macro elements should return a custom-sorted copy for you to repaste.

(It pushes to the end any selected birds not found in the custom guest list)

CSV sorted in custom order.kmmacros (5.9 KB)


Expand disclosure triangle to view JS source
return (() => {
    "use strict";

    // kmvar.local_CopiedCSV sorted by custom order specified in 
    // kmvar.local_PreOrderedCSV

    // Items not found in the custom ordering are pushed to the end.

    // main :: IO ()
    const main = () => {
        const
            rgxComma = /\s*,\s*/gu,
            keyOrder = kmvar.local_PreOrderedCSV
                .split(rgxComma)
                .reduce(
                    (a, k, i) => Object.assign(
                        a,
                        { [canonical(k)]: 1 + i }
                    ),
                    {}
                );

        return sortOn(
            k => keyOrder[canonical(k)] || Infinity
        )(
            kmvar.local_CopiedCSV.split(rgxComma)
        )
            .join(",");
    };


    // canonical :: String -> String
    const canonical = k =>
        k.split(/\W/u)
            .map(toLower)
            .join("");

    // --------------------- GENERIC ---------------------

    // comparing :: Ord a => (b -> a) -> b -> b -> Ordering
    const comparing = f =>
        // The ordering of f(x) and f(y) as a value
        // drawn from {-1, 0, 1}, representing {LT, EQ, GT}.
        x => y => {
            const
                a = f(x),
                b = f(y);

            return a < b
                ? -1
                : a > b
                    ? 1
                    : 0;
        };


    // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
    const sortBy = f =>
        // A copy of xs sorted by the comparator function f.
        xs => xs.slice()
            .sort((a, b) => f(a)(b));


    // sortOn :: Ord b => (a -> b) -> [a] -> [a]
    const sortOn = f =>
        // Equivalent to sortBy(comparing(f)), but with f(x)
        // evaluated only once for each x in xs.
        // ('Schwartzian' decorate-sort-undecorate).
        xs => sortBy(
            comparing(x => x[0])
        )(
            xs.map(x => [f(x), x])
        )
            .map(x => x[1]);

    // toLower :: String -> String
    const toLower = s =>
        // Lower-case version of string.
        s.toLocaleLowerCase();

    return main();
})();

That's amazing thanks! For any birds in local_CopiedCSV not present in local_PreOrderedCSV, can they be excluded from local_CopiedCSV and outputted separately, say in an alert pop up?

We can partition the output into two separate variables:

  1. local_CustomSorted (sorted copy of selected birds already on the guest list)
  2. local_Extras (any birds in the selection that don't yet figure in the custom order)

CSV sorted in custom order (plus extras).kmmacros (9.3 KB)


Expand disclosure triangle to view JS source
return (() => {
    "use strict";

    // kmvar.local_CopiedCSV sorted by custom order specified in 
    // kmvar.local_PreOrderedCSV
    // Items not found in the custom ordering are pushed to the end.

    // main :: IO ()
    const main = () => {
        const
            rgxComma = /\s*,\s*/gu,
            commaSeparated = intercalate(","),
            keyOrder = kmvar.local_PreOrderedCSV
                .split(rgxComma)
                .reduce(
                    (a, k, i) => Object.assign(
                        a,
                        { [canonical(k)]: 1 + i }
                    ),
                    {}
                ),
            [sorted, extras] = bimap(
                compose(
                    commaSeparated,
                    sortOn(
                        k => keyOrder[canonical(k)] || Infinity
                    )
                )
            )(
                commaSeparated
            )(
                partition(
                    k => canonical(k) in keyOrder
                )(
                    kmvar.local_CopiedCSV.split(rgxComma)
                )
            );

        return {
            sorted,
            extras
        };
    };


    // canonical :: String -> String
    const canonical = k =>
        k.split(/\W/u)
            .map(toLower)
            .join("");

    // --------------------- GENERIC ---------------------

    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a =>
        // A pair of values, possibly of
        // different types.
        b => ({
            type: "Tuple",
            "0": a,
            "1": b,
            length: 2,
            *[Symbol.iterator]() {
                for (const k in this) {
                    if (!isNaN(k)) {
                        yield this[k];
                    }
                }
            }
        });


    // bimap :: (a -> b) -> (c -> d) -> (a, c) -> (b, d)
    const bimap = f =>
        // Tuple instance of bimap.
        // A tuple of the application of f and g to the
        // first and second values respectively.
        g => tpl => Tuple(f(tpl[0]))(
            g(tpl[1])
        );


    // comparing :: Ord a => (b -> a) -> b -> b -> Ordering
    const comparing = f =>
        // The ordering of f(x) and f(y) as a value
        // drawn from {-1, 0, 1}, representing {LT, EQ, GT}.
        x => y => {
            const
                a = f(x),
                b = f(y);

            return a < b
                ? -1
                : a > b
                    ? 1
                    : 0;
        };


    // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
    const compose = (...fs) =>
        // A function defined by the right-to-left
        // composition of all the functions in fs.
        fs.reduce(
            (f, g) => x => f(g(x)),
            x => x
        );


    // first :: (a -> b) -> ((a, c) -> (b, c))
    const first = f =>
        // A simple function lifted to one which applies
        // to a tuple, transforming only its first item.
        ([x, y]) => Tuple(f(x))(y);


    // intercalate :: String -> [String] -> String
    const intercalate = s =>
        // The concatenation of xs
        // interspersed with copies of s.
        xs => xs.join(s);

    // partition :: (a -> Bool) -> [a] -> ([a], [a])
    const partition = p =>
        // A tuple of two lists - those elements in
        // xs which match p, and those which do not.
        xs => [...xs].reduce(
            (a, x) => (
                p(x)
                    ? first
                    : second
            )(ys => [...ys, x])(a),
            Tuple([])([])
        );


    // 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))
        xy => Tuple(
            xy[0]
        )(
            f(xy[1])
        );


    // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
    const sortBy = f =>
        // A copy of xs sorted by the comparator function f.
        xs => xs.slice()
            .sort((a, b) => f(a)(b));


    // sortOn :: Ord b => (a -> b) -> [a] -> [a]
    const sortOn = f =>
        // Equivalent to sortBy(comparing(f)), but with f(x)
        // evaluated only once for each x in xs.
        // ('Schwartzian' decorate-sort-undecorate).
        xs => sortBy(
            comparing(x => x[0])
        )(
            xs.map(x => [f(x), x])
        )
            .map(x => x[1]);

    // toLower :: String -> String
    const toLower = s =>
        // Lower-case version of string.
        s.toLocaleLowerCase();

    return JSON.stringify(main(), null, 2);
})();

Amazing thanks!

1 Like