Need Help Sorting a List of Strings Based on Its Substring

Hey guys, I need a bit of help sorting a list. It’s a bit complicated because I need to sort based on a substring of each item in the list.

Here’s are a few items in my list:

5e9204033debb66ef9d06cbf__Board 1 | To Do
5e9210c43823383ffec282dd__Board 1 | Done
5e91deb9b5477c7bf6831ab9__Board 1 | List 1
5e91dec34033e73893cad871__Board 1 | List 2
5e9203d1918b11356052f065__Board 2 | To Do
5e91dfae766168544765167f__Board 2 | Test for Attachments
5e9203e3b964487863f7b4ed__Board 2 | Done

So I need to sort based on the substring AFTER the two underscores:
Board 1 | To Do

for example.

I know how to split the strings on the “__”, but how do I sort the main string based on it?

I suspect ASObjC is needed, and that is fine, even preferred.

TIA for any suggestions or guidance.

Easy peasy. :grin:

use sorter : script "Custom Iterative Ternary Merge Sort" -- <https://macscripter.net/viewtopic.php?pid=194430#p194430>

set listOfStrings to paragraphs of "5e9204033debb66ef9d06cbf__Board 1 | To Do
5e9210c43823383ffec282dd__Board 1 | Done
5e91dfae766168544765167f__Board 2 | Test for Attachments
5e9203e3b964487863f7b4ed__Board 2 | Done
5e91dec34033e73893cad871__Board 1 | List 2
5e9203d1918b11356052f065__Board 2 | To Do
5e91deb9b5477c7bf6831ab9__Board 1 | List 1"

script onTextItem2
	on isGreater(a, b)
		return (text item 2 of a > text item 2 of b)
	end isGreater
end script

set astid to AppleScript's text item delimiters
set AppleScript's text item delimiters to "__"
-- Sort items 1 thru -1 of listOfStrings using the custom comparer above.
tell sorter to sort(listOfStrings, 1, -1, {comparer:onTextItem2})
set AppleScript's text item delimiters to astid

listOfStrings
--> {"5e9210c43823383ffec282dd__Board 1 | Done", "5e91deb9b5477c7bf6831ab9__Board 1 | List 1", "5e91dec34033e73893cad871__Board 1 | List 2", "5e9204033debb66ef9d06cbf__Board 1 | To Do", "5e9203e3b964487863f7b4ed__Board 2 | Done", "5e91dfae766168544765167f__Board 2 | Test for Attachments", "5e9203d1918b11356052f065__Board 2 | To Do"}
1 Like

Sure, for a master like you!

I’m sure it works great, but I’m not sure how to use this.
I went to your URL, and see the script, but where do I put that script so that the use command can find it?

Thanks.

Same place you put all your script libraries: ~/Library/Script Libraries.

1 Like

As a footnote, in JavaScript for Automation we don’t of course, need a library – it’s enough to use a custom comparator function on the standard Array.sort method.

I would personally tend to write sortBy(comparing(snd)), as in:

(() => {
    'use strict';

    // sortedByBoard :: String -> String
    const sortedByBoard = s =>
        unlines(map(x => x.join('__'))(
            sortBy(comparing(snd))(
                map(splitOn('__'))(
                    lines(s)
                )
            )
        ));


    const main = () =>
        sortedByBoard(`5e9204033debb66ef9d06cbf__Board 1 | To Do
5e9210c43823383ffec282dd__Board 1 | Done
5e91deb9b5477c7bf6831ab9__Board 1 | List 1
5e91dec34033e73893cad871__Board 1 | List 2
5e9203d1918b11356052f065__Board 2 | To Do
5e91dfae766168544765167f__Board 2 | Test for Attachments
5e9203e3b964487863f7b4ed__Board 2 | Done`);



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

    // comparing :: (a -> b) -> (a -> a -> Ordering)
    const comparing = f =>
        x => y => {
            const
                a = f(x),
                b = f(y);
            return a < b ? -1 : (a > b ? 1 : 0);
        };

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

    // list :: StringOrArrayLike b => b -> [a]
    const list = xs =>
        // xs itself, if it is an Array,
        // or an Array derived from xs.
        Array.isArray(xs) ? (
            xs
        ) : Array.from(xs);

    // map :: (a -> b) -> [a] -> [b]
    const map = f =>
        // The list obtained by applying f
        // to each element of xs.
        // (The image of xs under f).
        xs => list(xs).map(f);

    // snd :: (a, b) -> b
    const snd = tpl => tpl[1];

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


    // splitOn :: String -> String -> [String]
    const splitOn = pat => src =>
        // A list of the strings delimited by
        //   instances of a given pattern in src
        src.split(pat);

    // 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();
})();

Or if deep indentation feels unheimlich, you could instead write:

// sortedByBoard :: String -> String
const sortedByBoard = s =>
    compose(
        unlines,
        map(x => x.join('__')),
        sortBy(comparing(snd)),
        map(splitOn('__')),
        lines
    )(s)

by adding a compose function, defined as something like:

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

Hi Jim.

Sorry for not being more explicit about how to install the sort script. Also for this slow response. (It’s just been night time over here.)

If you decide to go with it, and need more gen on its customisation parameters, let me know.

@NigelGarvey I was thinking about an entry like 5e9210c43823383ffec282dd__Board 10 | Done, where some kind of numeric comparison is needed. I assumed that wrapping the comparator in an ignoring numeric strings block would do the job, but it appears not. Any thoughts?

Hi Shane.

*considering* numeric strings is the term to use if numeric comparisons are needed. The statement can be put either around the comparison code or around the call to the sort handler.

Nigel,

Many thanks for a great solution, particularly if I had a list of many thousands of strings to be sorted. Your script looks very fast and very powerful.
I have added your script to my Script Libraries (dud!), ready for future use.

For my use case, I have only a few hundred (at the most) strings to sort, and I’d like to avoid use of a script lib if I can.

So, all you ASObjC masters, can you please help me with this:

I have a good (great) solution using @ShaneStanley’s great BridgePlus Lib, which requires only one line of code. Can anyone convert that BP call to plain ASObjC?

Many TIA.

Script Using BridgePlus

property ptyScriptName : "Sort List of Strings by Substring"
property ptyScriptVer : "1.0"
property ptyScriptDate : "2020-05-16"
property ptyScriptAuthor : "JMichaelTX"

use bpLib : script "BridgePlus"
load framework

set boardList to paragraphs of "5e9204033debb66ef9d06cbf__Board 1 | To Do
5e9210c43823383ffec282dd__Board 1 | Done
5e91dfae766168544765167f__Board 2 | Test for Attachments
5e9203e3b964487863f7b4ed__Board 2 | Done
5e91dec34033e73893cad871__Board 1 | List 2
5e9203d1918b11356052f065__Board 2 | To Do
5e91deb9b5477c7bf6831ab9__Board 1 | List 1"

--- Split String into ID and Board Name ---
repeat with oBoard in boardList
  set contents of oBoard to my split(oBoard, "__")
end repeat

-----------------------------------------------------------------------------------------------
### CAN THIS BE REPLACED WITH JUST ASObjC? ###
set boardListSorted to bpLib's sublistsIn:boardList sortedByIndexes:{2} ascending:{true} sortTypes:{}
-----------------------------------------------------------------------------------------------

--- Join Back to Strings ---
repeat with oBoard in boardListSorted
  set contents of oBoard to my join(oBoard, "__")
end repeat

--- Return as String ---
return my join(boardListSorted, linefeed)

--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
on split(pString, pDelim) -- @Strings @Lists
  (*  VER: 1.0    2016-02-25
  ---------------------------------------------------------------------------------
    PURPOSE:  Splits a String to Create an List (Array)
  —————————————————————————————————————————————————————————————————————————————————
  *)
  set textDelimSave to AppleScript's text item delimiters
  set AppleScript's text item delimiters to pDelim
  
  set newList to text items of pString
  
  set AppleScript's text item delimiters to textDelimSave
  
  return newList
  
end split
--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
on join(pList, pDelim) -- @Lists @Strings
  (*  VER: 1.0   2016-02-25
  ---------------------------------------------------------------------------------
    PURPOSE:  Joins the items of a List (array) to create a string
  —————————————————————————————————————————————————————————————————————————————————
  *)
  set textDelimSave to AppleScript's text item delimiters
  set AppleScript's text item delimiters to pDelim
  
  set newString to pList as string
  
  set AppleScript's text item delimiters to textDelimSave
  
  return newString
  
end join
--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


Thanks. It came to me just after I shut down for the night.

The reason it’s in BridgePlus Lib is because the Objective-C way of doing it, which also uses a comparator, isn’t ASObjC compatible.

The job can be done in ASObjC, but it’s much, much slower than Nigel’s library — by a factor of maybe 70 times. If that’s not an issue, you could do this:

use AppleScript version "2.5" -- macOS 10.11 or later
use framework "Foundation"
use scripting additions

set listOfStrings to paragraphs of "5e9204033debb66ef9d06cbf__Board 1 | To Do
5e9210c43823383ffec282dd__Board 11 | Done
5e91deb9b5477c7bf6831ab9__Board 1 | List 1
5e91dec34033e73893cad871__Board 1 | List 2
5e9203d1918b11356052f065__Board 2 | To Do
5e91dfae766168544765167f__Board 2 | Test for Attachments
5e9203e3b964487863f7b4ed__Board 2 | Done"
set listOfStrings to current application's NSArray's arrayWithArray:listOfStrings
set theDict to current application's NSMutableDictionary's dictionary()
repeat with aString in listOfStrings
	(theDict's setObject:((aString's componentsSeparatedByString:"__")'s lastObject()) forKey:aString)
end repeat
set theResult to (theDict's keysSortedByValueUsingSelector:"localizedStandardCompare:") as list -- or just "compare:"

For kicks, I wrote a version using SQLite Lib2:

use AppleScript version "2.5" -- El Capitan (10.11) or later
use script "SQLite Lib2" version "1.0.0"
use scripting additions

set listOfStrings to "5e9204033debb66ef9d06cbf__Board 1 | To Do
5e9210c43823383ffec282dd__Board 1 | Done
5e91deb9b5477c7bf6831ab9__Board 1 | List 1
5e91dec34033e73893cad871__Board 1 | List 2
5e9203d1918b11356052f065__Board 2 | To Do
5e91dfae766168544765167f__Board 2 | Test for Attachments
5e9203e3b964487863f7b4ed__Board 2 | Done"
-- create and open in memory db
set theDb to open db in file missing value with can create
update db theDb sql string "create table test (a, b)"
batch string update db theDb sql string "insert into test values (?, ?)" with data listOfStrings using delimiter "__"
set theResult to query db theDb sql string "select a||'__'||b from test order by b"
close db theDb
set {saveTID, AppleScript's text item delimiters} to {AppleScript's text item delimiters, {linefeed}}
set theResult to paragraphs of (theResult as text)
set AppleScript's text item delimiters to saveTID
return theResult

Amusingly enough, it’s about three times faster than the ASObjC version.

Interesting topic!

Is it a typo? Here, Nigel’s script is “only” about 7 times faster than yours.

:rofl:

I think Shane’s first script may be sped up a bit by looping through the original list of texts rather than through an array:

use AppleScript version "2.5" -- macOS 10.11 or later
use framework "Foundation"
use scripting additions

set listOfStrings to paragraphs of "5e9204033debb66ef9d06cbf__Board 1 | To Do
5e9210c43823383ffec282dd__Board 11 | Done
5e91deb9b5477c7bf6831ab9__Board 1 | List 1
5e91dec34033e73893cad871__Board 1 | List 2
5e9203d1918b11356052f065__Board 2 | To Do
5e91dfae766168544765167f__Board 2 | Test for Attachments
5e9203e3b964487863f7b4ed__Board 2 | Done"

set theDict to current application's NSMutableDictionary's dictionary()
set astid to AppleScript's text item delimiters
set AppleScript's text item delimiters to "__"
repeat with aString in listOfStrings
	(theDict's setObject:(aString's last text item) forKey:aString)
end repeat
set AppleScript's text item delimiters to astid
set theResult to (theDict's keysSortedByValueUsingSelector:"localizedStandardCompare:") as list -- or just "compare:"

One feature of Shane’s method, if it’s relevant, is that it removes any duplicate entries.

1 Like

No, but it depends a lot on where you time it.

Nigel, you’re amazing!

How do you evaluate when “vanilla” code should be faster than AppleScriptObjC?
Is it allways because it’ s not bridged?
Or when it’ s about text or numbers?

Do you mean the computer or the app?
Here is what Script Geek returns with Nigel’s lib as first script and yours (modified by Nigel) as the second.

Another thing to note is that those test lists are pretty much sorted anyway, which gives my vanilla sort not very much to do. :slight_smile: Shane’s has to do all the bridging, no matter what.

Thanks Shane and Nigel.

Of course this test has a very small sample size, but even so it ran so fast that SD showed 0 sec:
image

When I get some time, I’ll gin up a test case of a few hundred rows and see how that compares. I don’t think it will change much.

That is a good bonus feature to have, and one that I actually can use.

I think for my use case of a few hundred rows, or less, and avoiding use of a script lib, this is probably the best solution. But I will definitely keep Nigel’s original sort (“vanilla”?) in my tool kit for more demanding cases.

Thanks to all for your suggestions, and a great discussion.