Toggle sort text lines in clipboard Aa-Zz ⇄ Zz-Aa

There is probably a more Maestronic way of doing this, but FWIW here is a macro which sorts text lines in the clipboard (case-insensitive). If you fire it again it sorts Z-A, toggling each time.

Toggle-sort clipboard contents (AZ ⇄ ZA).kmmacros (19.0 KB)

(function () {
    'use strict';

    var fnAaZz = function (a, b) {
            return a.toLowerCase()
                .localeCompare(
                    b.toLowerCase()
                );
        },
        fnZzAa = function (a, b) {
            return b.toLowerCase()
                .localeCompare(
                    a.toLowerCase()
                );
        };

    var a = Application.currentApplication(),
        sa = (a.includeStandardAdditions = true, a),
        strClip = sa.theClipboard(),
        lstLines = (
            strClip && typeof strClip === 'string'
        ) ? strClip.split(/[\n\r]+/) : [],
        strLines = lstLines.length ? lstLines.join('\n') : undefined,
        strSorted = strLines ? lstLines.sort(fnAaZz).join('\n') : undefined;

    return strSorted ? (function () {
        // Toggle AZ <> ZA if already sorted
        var blnZA = (strLines === strSorted),
            strOut = blnZA ? lstLines.sort(fnZzAa).join('\n') : strSorted;

        sa.setTheClipboardTo(
            strOut
        );

        sa.displayNotification(strOut, {
            withTitle: "Clipboard sorted    " + (blnZA ? "Z-A" : "A-Z")
        });

        return strOut;
    })() : '';
})();
3 Likes

Rob, thanks for this macro and JXA code.

If I have a list (array) of text already sorted, and I want to add a new element at the proper sort location, what would be the best way to do that?

Array.sort() will handle that efficiently.

lstSorted.concat(x).sort()
[1, 2, 3, 4, 5].concat(3.5).sort()

// --> [1, 2, 3, 3.5, 4, 5]
1 Like

Thanks, Rob.

I had done quite a bit of research, and did not see a clear "winner".
A lot of posters at stackoverflow don't seem to like the Array.sort() function.
But maybe that is for large, long lists.

Your approach seems to work well for me.

On the typical scales of the scripting context (smallish numbers of users per coder, and modest-sized datasets), coder time is much more scarce and valuable than run-time.

(Browser competition means that JavaScript engines are pretty heavily optimised, well beyond what casual scripting really needs, on the whole)

1 Like

As a footnote to the general mechanics sorting with Execute JavaScript for Automation actions, I have put some basic examples at:

http://rosettacode.org/wiki/Sort_using_a_custom_comparator#ES6

The examples are in ES6 JS, if you are running a pre-Sierra system you can obtain the ES5 equivalent by pasting the ES6 code into the left-hand panel at:

I’ve added ES5 source there now, and also included, with a couple of examples a mappendOrdering function, which makes it easier to write secondary (+) sorts like ‘by descending length and then AZ’

http://rosettacode.org/wiki/Sort_using_a_custom_comparator#JavaScript

Can someone kindly share with me a version of the script that doesn't have the sort toggle option? It's driving me nuts! I just need it to sort in ascending order.

TIA!

Kick these tires:

Toggle-sort clipboard contents (AZ only).kmmacros (18.7 KB)

2 Likes

Works great. I tried modifying it myself but my JavaScript knowledge is close to nil.

Thank you so much!

You're welcome!

1 Like

These days, FWIW, | would probably use an option type to define what happens if the clipboard contents isn't textual.

either(msg => msg)(copyText)(
    bindLR(clipTextLR())(
        compose(
            Right,
            unlines,
            sortBy(
                a => b => a.localeCompare(b)
            ),
            lines
        )
    )

Sort text in clipboard (AZ).kmmacros (21.0 KB)

Full JS source
(() => {
    'use strict';

    // Rob Trew 2020

    ObjC.import('AppKit');

    const main = () =>
        either(msg => msg)(copyText)(
            bindLR(clipTextLR())(
                compose(
                    Right,
                    unlines,
                    sortBy(
                        a => b => a.localeCompare(b)
                    ),
                    lines
                )
            )
        );


    //  ----------------------- JXA -----------------------

    // clipTextLR :: () -> Either String String
    const clipTextLR = () => {
        const
            v = ObjC.unwrap($.NSPasteboard.generalPasteboard
                .stringForType($.NSPasteboardTypeString));
        return Boolean(v) && v.length > 0 ? (
            Right(v)
        ) : Left('No utf8-plain-text found in clipboard.');
    };

    // copyText :: String -> IO String
    const copyText = s => {
        // String copied to general pasteboard.
        const pb = $.NSPasteboard.generalPasteboard;
        return (
            pb.clearContents,
            pb.setStringForType(
                $(s),
                $.NSPasteboardTypeString
            ),
            s
        );
    };


    // --------------------- GENERIC ----------------------
    //  https://github.com/RobTrew/prelude-jxa

    // Left :: a -> Either a b
    const Left = x => ({
        type: 'Either',
        Left: x
    });


    // Right :: b -> Either a b
    const Right = x => ({
        type: 'Either',
        Right: x
    });


    // bindLR (>>=) :: Either a ->
    // (a -> Either b) -> Either b
    const bindLR = m =>
        mf => undefined !== m.Left ? (
            m
        ) : mf(m.Right);


    // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
    const compose = (...fs) =>
        fs.reduce(
            (f, g) => x => f(g(x)),
            x => x
        );


    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl =>
        fr => e => 'Either' === e.type ? (
            undefined !== e.Left ? (
                fl(e.Left)
            ) : fr(e.Right)
        ) : undefined;


    // lines :: String -> [String]
    const lines = s =>
        // A list of strings derived from a single
        // newline-delimited string.
        0 < s.length ? (
            s.split(/[\r\n]/)
        ) : [];


    // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
    const sortBy = f =>
        xs => xs.slice()
        .sort((a, b) => f(a)(b));


    // unlines :: [String] -> String
    const unlines = xs =>
        // A single string formed by the intercalation
        // of a list of strings with the newline character.
        xs.join('\n');

    // MAIN ---
    return main();
})();
3 Likes

Hi,

sorry to revive a dead thread, but hoping to avoid making a new one.

Is there a way to edit this such that hyperlinks are maintained?

Not sure I understand the question – you are hoping to sort rich text rather than plain text ?

To give you an answer, I would need so see:

  1. an example of an input text with hyperlinks of the kind that you have in mind,
  2. the sorted output that you saw, and
  3. an example of what you expected.

To give you an answer, I would need so see:

  1. an example of an input text with hyperlinks of the kind that you have in mind,
    image
  1. the sorted output that you saw, and
    image
  1. an example of what you expected.
    image

Here you go!

In order for me to understand what I'm looking at in your first screenshot, I would like you to run the following action when your clipboard contains the data you are trying to process.

image

Then paste the result in this thread. You see, I can't really tell what you are dealing with in your first image. If you do the above, it will tell me byte for byte what your input contains. I'm concerned that your input isn't really hyperlinks, just highlighted text.

Then paste the result in this thread. You see, I can't really tell what you are dealing with in your first image.

When I click on the text I get redirected to a website, so I suspect they are hyperlinks. I'll post in a second.

EDIT:

Here is what I get

I'm 99.999% sure that you accidentally used this action:

image

instead of this action:

image

Can you see the difference? Can you try again with the correct action?

I do, thanks! Here is the text

It says right at the top of your window that you are still using the action called "Javascript for Automation" rather than "Execute Shell Script." I recommend that you try again. I'm likely to be gone for the rest of the day, so someone else may have to help.