Extract Data to Columns

Could you give me a file or two in which that happens ?

In the songList.txt which I have here, the first two lines are:

1 3 RAINDROPS KEEP FALLIN’ ON MY HEAD –•– B.J. Thomas (Scepter)-10 (1 week at #1) (1) 
2 2 LEAVING ON A JET PLANE –•– Peter, Paul and Mary (Warner Brothers)-11 (1) 

and both of those seem to get through to the tab-separated clipboard, and into Numbers.

Are you seeing the same ?

Scratch that – for some reason I hadn't looked in the new zip ...

Well, it turns out that there's an interesting quirk in the data:

The lines that are getting lost may well be following week at #1 predecessors, but the unexpected thing about them turns out to be that:

  • most lines don't have the artist in parentheses,
  • but these occasional lines do, and that has been evading the pattern recognition

See the parenthesisation of Weissberg & Mandell in the 2nd line below.

1 5 KILLING ME SOFTLY WITH HIS SONG –•– Roberta Flack (Atlantic)-5 (1 week at #1) (1) 
2 4 DUELING BANJOS –•– Deliverance (Eric Weissberg and Steve Mandell) (Warner Brothers)-7 (2) 
3 1 CROCODILE ROCK –•– Elton John (MCA)-12 (1) 

Should I assume that that is just a quirk of the source (perhaps something that arises when the track has a context like a film as well as an artist?) and allow for it downstream in the parsing ?

Wow. I did not notice that quirk at all. But I've been looking at 60 years worth :face_with_raised_eyebrow:. Sorry about that. Is there any way to allow for that quirk? I'm sorry to be causing you so much work. I hope they are paying you well there.

The one I was just about to send you has the same quirk. I'll go with whatever is easiest for you. I would like to retain the Last Week, This Week, Artist, Label, # Weeks on Chart, Chart Peak. I could easily do without the (# of weeks at #1). Is it possible to allow for that 'quirk' without causing too much trouble?

No problem – it's interesting wax-on wax-off practice : - )

I'll take a look at a lookahead pattern tomorrow morning.

I have to say I love your attitude!

Thanks again and again.

Next iteration:

(perhaps a little slower because of lookahead and backtracking ?)

Chart rows from file path D.kmmacros (36.9 KB)

JS Source
(() => {
    'use strict';

    ObjC.import('AppKit');

    // Draft 0.7
    // Removed debugging version of bindP, 
    // and replaced it with a simple version.

    // main :: IO ()
    const main = () => {
        const
            kme = Application('Keyboard Maestro Engine'),
            fpSongList = kme.getvariable('songListFilePath');
        return either(
            msg => alert('Parsing chart rows')(msg)
        )(
            tabbedLines => (
                copyText(tabbedLines),
                tabbedLines
            )
        )(
            bindLR(
                readFileLR(fpSongList)
                //clipTextLR()
            )(
                txtRows => txtRows.includes('–•–') ? (() => {
                    const rowParse = parse(chartListRow());
                    return Right(
                        lines(txtRows)
                        .flatMap(rowParse)
                        .map(
                            pair => fst(pair).join('\t')
                        )
                        .join('\n')
                    );
                })() : Left('File contains no –•– lines.')
            )
        );
    };

    // ------------------- TRACK ROWS --------------------

    // chartListRow :: Parser [String]
    const chartListRow = () => {
        const
            integer = unsignedIntP(),
            saucer = string('–•–'),
            parenthesized = between(
                char('(')
            )(
                char(')')
            )(
                many(satisfy(c => c !== ')'))
            ),
            quoted = cs => `"${concat(cs).trim()}"`;

        return bindP(
            integer
        )(tw => bindP(
            integer
        )(lw => bindP(
            fmapP(quoted)(manyTill(item())(saucer))
        )(title => bindP(
            fmapP(cs => {
                const
                    parsed = parse(some(token(
                        altP(
                            parenthesized
                        )(
                            some(satisfy(c => '(' !== c))
                        )
                    )))(concat(cs));
                return 0 < parsed.length ? (
                    parsed[0][0].map(
                        cs => concat(cs).trim()
                    )
                ) : [];
            })(
                manyTill(item())(
                    lookAhead(
                        sequenceP([char('-'), digit()])
                    )
                )
            )
        )(attributions => bindP(
            char('-')
        )(_ => bindP(
            integer
        )(
            weeksInChart => bindP(
                fmapP(reverse)(
                    some(fmapP(concat)(
                        token(parenthesized)
                    ))
                )
            )(
                peakOne => pureP([
                    concat(tw),
                    concat(lw),
                    title,
                    quoted(init(attributions).join(' – ')),
                    quoted(last(attributions))
                ].concat(
                    1 < peakOne.length ? (
                        peakOne
                    ) : peakOne.concat('')
                ))
            )
        ))))))
    };

    // --------------------- PARSERS ---------------------

    // Parser :: String -> [(a, String)] -> Parser a
    const Parser = f =>
        // A function lifted into a Parser object.
        ({
            type: 'Parser',
            parser: f
        });


    // altP (<|>) :: Parser a -> Parser a -> Parser a
    const altP = p =>
        // p, or q if p doesn't match.
        q => Parser(s => {
            const xs = parse(p)(s);
            return 0 < xs.length ? (
                xs
            ) : parse(q)(s);
        });


    // apP <*> :: Parser (a -> b) -> Parser a -> Parser b
    const apP = pf =>
        // A new parser obtained by the application 
        // of a Parser-wrapped function,
        // to a Parser-wrapped value.
        p => Parser(
            s => parse(pf)(s).flatMap(
                vr => parse(
                    fmapP(vr[0])(p)
                )(vr[1])
            )
        );

    // between :: Parser open -> Parser close -> 
    // Parser a -> Parser a
    const between = pOpen =>
        // A version of p which matches between 
        // pOpen and pClose (both discarded).
        pClose => p => thenBindP(pOpen)(
            p
        )(
            compose(thenP(pClose), pureP)
        );


    // bindP (>>=) :: Parser a -> 
    // (a -> Parser b) -> Parser b
    const bindP = p =>
        // A new parser obtained by the application of 
        // a function to a Parser-wrapped value.
        // The function must enrich its output, lifting it 
        // into a new Parser.
        // Allows for the nesting of parsers.
        f => Parser(
            s => parse(p)(s).flatMap(
                tpl => parse(f(tpl[0]))(tpl[1])
            )
        );

    // char :: Char -> Parser Char
    const char = x =>
        // A particular single character.
        satisfy(c => x == c);


    // digit :: Parser Char
    const digit = () =>
        // A single digit.
        satisfy(isDigit);


    // fmapP :: (a -> b) -> Parser a -> Parser b  
    const fmapP = f =>
        // A new parser derived by the structure-preserving 
        // application of f to the value in p.
        p => Parser(
            s => parse(p)(s).flatMap(
                first(f)
            )
        );


    // item :: () -> Parser Char
    const item = () =>
        // A single character.
        // Synonym of anyChar.
        Parser(
            s => 0 < s.length ? [
                Tuple(s[0])(
                    s.slice(1)
                )
            ] : []
        );


    // liftA2P :: (a -> b -> c) -> 
    // Parser a -> Parser b -> Parser c
    const liftA2P = op =>
        // The binary function op, lifted
        // to a function over two parsers.
        p => apP(fmapP(op)(p));


    // lookAhead :: Parser a -> Parser a
    const lookAhead = p =>
        // A version of p which parses 
        // without consuming.
        Parser(
            s => p.parser(s).flatMap(
                second(_ => s)
            )
        );


    // many :: Parser a -> Parser [a]
    const many = p => {
        // Zero or more instances of p.
        // Lifts a parser for a simple type of value 
        // to a parser for a list of such values.
        const some_p = p =>
            liftA2P(
                x => xs => [x].concat(xs)
            )(p)(many(p));
        return Parser(
            s => parse(
                0 < s.length ? (
                    altP(some_p(p))(pureP([]))
                ) : pureP([])
            )(s)
        );
    };

    // manyTill :: Parser a -> Parser e -> Parser [a]
    const manyTill = p =>
        // All of the matches for p before e matches.
        // Wrapping e in lookAhead can preserve any 
        // string which matches e, if it is needed.
        e => {
            const
                scan = () => altP(
                    thenP(e)(pureP([]))
                )(
                    bindP(
                        p
                    )(x => bindP(
                        go
                    )(xs => pureP(
                        [x].concat(xs)
                    )))
                ),
                go = scan();
            return go;
        };


    // oneOf :: [Char] -> Parser Char
    const oneOf = s =>
        // One instance of any character found
        // the given string.
        satisfy(c => s.includes(c));

    // parse :: Parser a -> String -> [(a, String)]
    const parse = p =>
        // The result of parsing a string with p.
        p.parser;

    // pureP :: a -> Parser a
    const pureP = x =>
        // The value x lifted, unchanged, 
        // into the Parser monad.
        Parser(s => [Tuple(x)(s)]);

    // satisfy :: (Char -> Bool) -> Parser Char
    const satisfy = test =>
        // Any character for which the 
        // given predicate returns true.
        Parser(
            s => 0 < s.length ? (
                test(s[0]) ? [
                    Tuple(s[0])(s.slice(1))
                ] : []
            ) : []
        );

    // sequenceP :: [Parser a] -> Parser [a]
    const sequenceP = ps =>
        // A single parser for a list of values, derived
        // from a list of parsers for single values.
        Parser(
            s => ps.reduce(
                (a, q) => a.flatMap(
                    vr => parse(q)(snd(vr)).flatMap(
                        first(xs => fst(vr).concat(xs))
                    )
                ),
                [Tuple([])(s)]
            )
        );

    // some :: Parser a -> Parser [a]
    const some = p => {
        // One or more instances of p.
        // Lifts a parser for a simple type of value 
        // to a parser for a list of such values.
        const many_p = p =>
            altP(some(p))(pureP([]));
        return Parser(
            s => parse(
                liftA2P(
                    x => xs => [x].concat(xs)
                )(p)(many_p(p))
            )(s)
        );
    };

    // string :: String -> Parser String
    const string = s =>
        // A particular string.
        fmapP(cs => cs.join(''))(
            sequenceP([...s].map(char))
        );

    // thenBindP :: Parser a -> Parser b -> 
    // (b -> Parser c) Parser c
    const thenBindP = o =>
        // A combination of thenP and bindP in which a 
        // preliminary  parser consumes text and discards
        // its output, before any output of a subsequent
        // parser is bound.
        p => f => Parser(
            s => parse(o)(s).flatMap(
                vr => parse(p)(vr[1]).flatMap(
                    tpl => parse(f(tpl[0]))(tpl[1])
                )
            )
        );

    // thenP (>>) :: Parser a -> Parser b -> Parser b
    const thenP = o =>
        // A composite parser in which o just consumes text
        // and then p consumes more and returns a value.
        p => Parser(
            s => parse(o)(s).flatMap(
                vr => parse(p)(vr[1])
            )
        );

    // token :: Parser a -> Parser a
    const token = p => {
        // A new parser for a space-wrapped 
        // instance of p. Any flanking 
        // white space is discarded.
        const space = whiteSpace();
        return between(space)(space)(p);
    };

    // parse :: Parser Int
    const unsignedIntP = () =>
        token(some(digit()))

    // whiteSpace :: Parser String
    const whiteSpace = () =>
        // Zero or more non-printing characters.
        many(oneOf(' \t\n\r'));


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

    // alert :: String -> String -> IO String
    const alert = title =>
        s => {
            const sa = Object.assign(
                Application('System Events'), {
                    includeStandardAdditions: true
                });
            return (
                sa.activate(),
                sa.displayDialog(s, {
                    withTitle: title,
                    buttons: ['OK'],
                    defaultButton: 'OK'
                }),
                s
            );
        };

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


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


    // readFileLR :: FilePath -> Either String IO String
    const readFileLR = fp => {
        // Either a message or the contents of any
        // text file at the given filepath.
        const
            e = $(),
            ns = $.NSString
            .stringWithContentsOfFileEncodingError(
                $(fp).stringByStandardizingPath,
                $.NSUTF8StringEncoding,
                e
            );
        return ns.isNil() ? (
            Left(ObjC.unwrap(e.localizedDescription))
        ) : Right(ObjC.unwrap(ns));
    };

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

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


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


    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a =>
        b => ({
            type: 'Tuple',
            '0': a,
            '1': b,
            length: 2
        });


    // 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) =>
        // 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
        );


    // concat :: [[a]] -> [a]
    // concat :: [String] -> String
    const concat = xs => (
        ys => 0 < ys.length ? (
            ys.every(Array.isArray) ? (
                []
            ) : ''
        ).concat(...ys) : ys
    )(list(xs));


    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl =>
        // Application of the function fl to the
        // contents of any Left value in e, or
        // the application of fr to its Right value.
        fr => e => 'Either' === e.type ? (
            undefined !== e.Left ? (
                fl(e.Left)
            ) : fr(e.Right)
        ) : undefined;


    // 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.
        xy => {
            const tpl = Tuple(f(xy[0]))(xy[1]);
            return Array.isArray(xy) ? (
                Array.from(tpl)
            ) : tpl;
        };


    // fst :: (a, b) -> a
    const fst = tpl =>
        // First member of a pair.
        tpl[0];


    // init :: [a] -> [a]
    const init = xs => (
        // All elements of a list except the last.
        ys => 0 < ys.length ? (
            ys.slice(0, -1)
        ) : undefined
    )(list(xs));


    // isDigit :: Char -> Bool
    const isDigit = c => {
        const n = c.codePointAt(0);
        return 48 <= n && 57 >= n;
    };


    // last :: [a] -> a
    const last = xs =>
        // The last item of a list.
        0 < xs.length ? (
            xs.slice(-1)[0]
        ) : undefined;


    // lines :: String -> [String]
    const lines = s =>
        // A list of strings derived from a single
        // string delimited by newline and or CR.
        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 || []);


    // reverse :: [a] -> [a]
    const reverse = xs =>
        'string' !== typeof xs ? (
            xs.slice(0).reverse()
        ) : xs.split('').reverse().join('');


    // showLog :: a -> IO ()
    const showLog = (...args) =>
        console.log(
            args
            .map(JSON.stringify)
            .join(' -> ')
        );

    // 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 => {
            const tpl = Tuple(xy[0])(f(xy[1]));
            return Array.isArray(xy) ? (
                Array.from(tpl)
            ) : tpl;
        };

    // sj :: a -> String
    function sj() {
        // Abbreviation of showJSON for quick testing.
        // Default indent size is two, which can be
        // overriden by any integer supplied as the
        // first argument of more than one.
        const args = Array.from(arguments);
        return JSON.stringify.apply(
            null,
            1 < args.length && !isNaN(args[0]) ? [
                args[1], null, args[0]
            ] : [args[0], null, 2]
        );
    }


    // snd :: (a, b) -> b
    const snd = tpl =>
        // Second member of a pair.
        tpl[1];

    // MAIN 
    return main();
})();

Well, it doesn't seem to be any slower to me, but now it's taking out a whole column. Attached is the spreadsheet run with B & D versions. B is orange, and D is gray. With the song list.
If you are out of patience with this project I completely understand.

I guess I could do a search and replace for those quirks where the Artist Name is in parens??

I got the 'Cookbook' and your file. I'll study it and see if I can learn enough to get finished.

Thanks so much for your effort. You are a Rock Star!

Thanks,

Wendy

Archive 3.zip (160 KB)

Step by step – no hurry : -)

( Restored the writing out of weeksInChart – forgotten while reworking attributions )

next iteration:

Chart rows from file path E.kmmacros (36.1 KB)

JS Source
(() => {
    'use strict';

    ObjC.import('AppKit');

    // Draft 0.10
    // Restored export of weeksInChart field.
    // Tidied

    // main :: IO ()
    const main = () =>
        either(
            alert('Parsing chart rows')
        )(
            tabbedLines => (
                copyText(tabbedLines),
                tabbedLines
            )
        )(
            bindLR(
                readFileLR(
                    Application('Keyboard Maestro Engine')
                    .getvariable('songListFilePath')
                )
            )(
                txtRows => txtRows.includes('–•–') ? (
                    Right(
                        lines(txtRows).flatMap(
                            parse(chartListRow())
                        )
                        .map(
                            pair => fst(pair).join('\t')
                        )
                        .join('\n')
                    )
                ) : Left('File contains no –•– lines.')
            )
        );


    // ------------------- TRACK ROWS --------------------

    // chartListRow :: Parser [String]
    const chartListRow = () => {
        const
            integer = unsignedIntP(),
            parenthesized = parens(
                many(satisfy(c => c !== ')'))
            ),
            quoted = cs => `"${concat(cs).trim()}"`;

        return bindP(
            integer
        )(tw => bindP(
            integer
        )(lw => bindP(
            fmapP(quoted)(
                manyTill(item())(string('–•–'))
            )
        )(title => bindP(
            fmapP(cs => {
                const
                    parsed = parse(some(token(
                        altP(
                            parenthesized
                        )(
                            some(satisfy(c => '(' !== c))
                        )
                    )))(concat(cs));
                return 0 < parsed.length ? (
                    parsed[0][0].map(
                        cs => concat(cs).trim()
                    )
                ) : [];
            })(
                manyTill(item())(
                    lookAhead(
                        sequenceP([char('-'), digit()])
                    )
                )
            )
        )(attributions => bindP(
            char('-')
        )(_ => bindP(
            integer
        )(weeksInChart => bindP(
            fmapP(reverse)(
                some(fmapP(concat)(
                    token(parenthesized)
                ))
            )
        )(peakOne => pureP([
            concat(tw),
            concat(lw),
            title,
            quoted(init(attributions).join(' – ')),
            quoted(last(attributions)),
            concat(weeksInChart)
        ].concat(
            1 < peakOne.length ? (
                peakOne
            ) : peakOne.concat('')
        )))))))))
    };

    // --------------------- PARSERS ---------------------

    // Parser :: String -> [(a, String)] -> Parser a
    const Parser = f =>
        // A function lifted into a Parser object.
        ({
            type: 'Parser',
            parser: f
        });


    // altP (<|>) :: Parser a -> Parser a -> Parser a
    const altP = p =>
        // p, or q if p doesn't match.
        q => Parser(s => {
            const xs = parse(p)(s);
            return 0 < xs.length ? (
                xs
            ) : parse(q)(s);
        });


    // apP <*> :: Parser (a -> b) -> Parser a -> Parser b
    const apP = pf =>
        // A new parser obtained by the application 
        // of a Parser-wrapped function,
        // to a Parser-wrapped value.
        p => Parser(
            s => parse(pf)(s).flatMap(
                vr => parse(
                    fmapP(vr[0])(p)
                )(vr[1])
            )
        );


    // between :: Parser open -> Parser close -> 
    // Parser a -> Parser a
    const between = pOpen =>
        // A version of p which matches between 
        // pOpen and pClose (both discarded).
        pClose => p => thenBindP(pOpen)(
            p
        )(
            compose(thenP(pClose), pureP)
        );


    // bindP (>>=) :: Parser a -> 
    // (a -> Parser b) -> Parser b
    const bindP = p =>
        // A new parser obtained by the application of 
        // a function to a Parser-wrapped value.
        // The function must enrich its output, lifting it 
        // into a new Parser.
        // Allows for the nesting of parsers.
        f => Parser(
            s => parse(p)(s).flatMap(
                tpl => parse(f(tpl[0]))(tpl[1])
            )
        );


    // char :: Char -> Parser Char
    const char = x =>
        // A particular single character.
        satisfy(c => x == c);


    // digit :: Parser Char
    const digit = () =>
        // A single digit.
        satisfy(isDigit);


    // fmapP :: (a -> b) -> Parser a -> Parser b  
    const fmapP = f =>
        // A new parser derived by the structure-preserving 
        // application of f to the value in p.
        p => Parser(
            s => parse(p)(s).flatMap(
                first(f)
            )
        );


    // item :: () -> Parser Char
    const item = () =>
        // A single character.
        // Synonym of anyChar.
        Parser(
            s => 0 < s.length ? [
                Tuple(s[0])(
                    s.slice(1)
                )
            ] : []
        );


    // liftA2P :: (a -> b -> c) -> 
    // Parser a -> Parser b -> Parser c
    const liftA2P = op =>
        // The binary function op, lifted
        // to a function over two parsers.
        p => apP(fmapP(op)(p));


    // lookAhead :: Parser a -> Parser a
    const lookAhead = p =>
        // A version of p which parses 
        // without consuming.
        Parser(
            s => p.parser(s).flatMap(
                second(_ => s)
            )
        );


    // many :: Parser a -> Parser [a]
    const many = p => {
        // Zero or more instances of p.
        // Lifts a parser for a simple type of value 
        // to a parser for a list of such values.
        const some_p = p =>
            liftA2P(
                x => xs => [x].concat(xs)
            )(p)(many(p));
        return Parser(
            s => parse(
                0 < s.length ? (
                    altP(some_p(p))(pureP([]))
                ) : pureP([])
            )(s)
        );
    };


    // manyTill :: Parser a -> Parser e -> Parser [a]
    const manyTill = p =>
        // All of the matches for p before e matches.
        // Wrapping e in lookAhead can preserve any 
        // string which matches e, if it is needed.
        e => {
            const
                scan = () => altP(
                    thenP(e)(pureP([]))
                )(
                    bindP(
                        p
                    )(x => bindP(
                        go
                    )(xs => pureP(
                        [x].concat(xs)
                    )))
                ),
                go = scan();
            return go;
        };


    // oneOf :: [Char] -> Parser Char
    const oneOf = s =>
        // One instance of any character found
        // the given string.
        satisfy(c => s.includes(c));


    // parens :: Parser a -> Parser b
    const parens = p =>
        // A parenthesized value of some kind.
        between(
            char('(')
        )(
            char(')')
        )(p);


    // parse :: Parser a -> String -> [(a, String)]
    const parse = p =>
        // The result of parsing a string with p.
        p.parser;


    // pureP :: a -> Parser a
    const pureP = x =>
        // The value x lifted, unchanged, 
        // into the Parser monad.
        Parser(s => [Tuple(x)(s)]);


    // satisfy :: (Char -> Bool) -> Parser Char
    const satisfy = test =>
        // Any character for which the 
        // given predicate returns true.
        Parser(
            s => 0 < s.length ? (
                test(s[0]) ? [
                    Tuple(s[0])(s.slice(1))
                ] : []
            ) : []
        );


    // sequenceP :: [Parser a] -> Parser [a]
    const sequenceP = ps =>
        // A single parser for a list of values, derived
        // from a list of parsers for single values.
        Parser(
            s => ps.reduce(
                (a, q) => a.flatMap(
                    vr => parse(q)(snd(vr)).flatMap(
                        first(xs => fst(vr).concat(xs))
                    )
                ),
                [Tuple([])(s)]
            )
        );


    // some :: Parser a -> Parser [a]
    const some = p => {
        // One or more instances of p.
        // Lifts a parser for a simple type of value 
        // to a parser for a list of such values.
        const many_p = p =>
            altP(some(p))(pureP([]));
        return Parser(
            s => parse(
                liftA2P(
                    x => xs => [x].concat(xs)
                )(p)(many_p(p))
            )(s)
        );
    };


    // string :: String -> Parser String
    const string = s =>
        // A particular string.
        fmapP(cs => cs.join(''))(
            sequenceP([...s].map(char))
        );


    // thenBindP :: Parser a -> Parser b -> 
    // (b -> Parser c) Parser c
    const thenBindP = o =>
        // A combination of thenP and bindP in which a 
        // preliminary  parser consumes text and discards
        // its output, before any output of a subsequent
        // parser is bound.
        p => f => Parser(
            s => parse(o)(s).flatMap(
                vr => parse(p)(vr[1]).flatMap(
                    tpl => parse(f(tpl[0]))(tpl[1])
                )
            )
        );


    // thenP (>>) :: Parser a -> Parser b -> Parser b
    const thenP = o =>
        // A composite parser in which o just consumes text
        // and then p consumes more and returns a value.
        p => Parser(
            s => parse(o)(s).flatMap(
                vr => parse(p)(vr[1])
            )
        );


    // token :: Parser a -> Parser a
    const token = p => {
        // A new parser for a space-wrapped 
        // instance of p. Any flanking 
        // white space is discarded.
        const space = whiteSpace();
        return between(space)(space)(p);
    };


    // parse :: Parser Int
    const unsignedIntP = () =>
        token(some(digit()))


    // whiteSpace :: Parser String
    const whiteSpace = () =>
        // Zero or more non-printing characters.
        many(oneOf(' \t\n\r'));


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

    // alert :: String -> String -> IO String
    const alert = title =>
        s => {
            const sa = Object.assign(
                Application('System Events'), {
                    includeStandardAdditions: true
                });
            return (
                sa.activate(),
                sa.displayDialog(s, {
                    withTitle: title,
                    buttons: ['OK'],
                    defaultButton: 'OK'
                }),
                s
            );
        };


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


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


    // readFileLR :: FilePath -> Either String IO String
    const readFileLR = fp => {
        // Either a message or the contents of any
        // text file at the given filepath.
        const
            e = $(),
            ns = $.NSString
            .stringWithContentsOfFileEncodingError(
                $(fp).stringByStandardizingPath,
                $.NSUTF8StringEncoding,
                e
            );
        return ns.isNil() ? (
            Left(ObjC.unwrap(e.localizedDescription))
        ) : Right(ObjC.unwrap(ns));
    };

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

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


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


    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a =>
        b => ({
            type: 'Tuple',
            '0': a,
            '1': b,
            length: 2
        });


    // 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) =>
        // 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
        );


    // concat :: [[a]] -> [a]
    // concat :: [String] -> String
    const concat = xs => (
        ys => 0 < ys.length ? (
            ys.every(Array.isArray) ? (
                []
            ) : ''
        ).concat(...ys) : ys
    )(list(xs));


    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl =>
        // Application of the function fl to the
        // contents of any Left value in e, or
        // the application of fr to its Right value.
        fr => e => 'Either' === e.type ? (
            undefined !== e.Left ? (
                fl(e.Left)
            ) : fr(e.Right)
        ) : undefined;


    // 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.
        xy => {
            const tpl = Tuple(f(xy[0]))(xy[1]);
            return Array.isArray(xy) ? (
                Array.from(tpl)
            ) : tpl;
        };


    // fst :: (a, b) -> a
    const fst = tpl =>
        // First member of a pair.
        tpl[0];


    // init :: [a] -> [a]
    const init = xs => (
        // All elements of a list except the last.
        ys => 0 < ys.length ? (
            ys.slice(0, -1)
        ) : undefined
    )(list(xs));


    // isDigit :: Char -> Bool
    const isDigit = c => {
        const n = c.codePointAt(0);
        return 48 <= n && 57 >= n;
    };


    // last :: [a] -> a
    const last = xs =>
        // The last item of a list.
        0 < xs.length ? (
            xs.slice(-1)[0]
        ) : undefined;


    // lines :: String -> [String]
    const lines = s =>
        // A list of strings derived from a single
        // string delimited by newline and or CR.
        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 || []);


    // reverse :: [a] -> [a]
    const reverse = xs =>
        'string' !== typeof xs ? (
            xs.slice(0).reverse()
        ) : xs.split('').reverse().join('');


    // 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 => {
            const tpl = Tuple(xy[0])(f(xy[1]));
            return Array.isArray(xy) ? (
                Array.from(tpl)
            ) : tpl;
        };


    // snd :: (a, b) -> b
    const snd = tpl =>
        // Second member of a pair.
        tpl[1];

    // MAIN 
    return main();
})();

Wow. A kindred spirit! I'm usually like a dog with a bone with this kind of stuff.

It is working on every list I throw at it!

I can't thank you enough. I pick up new hobbies all the time. My next one will definitely be learning JXA. This is fantastic.

Thank you VERY much. You have made my weekend!

Please keep up the good work.

Take care,

Wendy

:+1:

60 years worth

(don't neglect the 1950s !)

Oh no, never neglect the 50's!

You KNOW I'll be buggin you again. I can't get over how cool this is!! :sunglasses::clap:

: -)

PS a regular expression version, FWIW, might look roughly like this:

Chart rows Regex version.kmmacros (22.0 KB)

Swings and roundabouts – the regular expressions are possibly easier to write, but:

  • can be harder to read and adjust a week or two later
  • only return string types (not a problem in this case)
  • can't directly express recursive (nested) patterns (also not a problem here)

This one works too, although I see exactly why you might prefer the JXA. I’ve been building a lot of iOS Shortcuts and am also fascinated with the uses for Regex, but the JXA does look easier to read.

1 Like