Extract Data to Columns

Hi,

I am working on a bunch of spreadsheets that I would like to split the text to columns in.

Tha columns are:
TW LW TITLE –•– Artist (Label) Weeks on Chart (Peak To Date)

This is a sample of the data:

1 3 RAINDROPS KEEP FALLIN’ ON MY HEAD –•– B.J. Thomas (Scepter)-10 (1 week at #1) (1)

I have tried 'searching' and replacing so that I could split the columns, but that is not working. I am thinking I would need a regular expression to search for 'strings within strings' to create new columns and then move each small bit of data, but regex looks like Greek to me. Is there some Newbie manual somewhere that I could look for an answer in???

There are various ways of doing that, but in the meanwhile, could you clarify:

What is the -10 in:

1 3 RAINDROPS KEEP FALLIN’ ON MY HEAD –•– B.J. Thomas (Scepter)-10(1 week at #1) (1)

and how does in fit into the schema:

TW LW TITLE –•– Artist (Label) Weeks on Chart (Peak To Date)

which seems to:

  • Go straight from (Label) to Weeks on Chart
  • and appears to have three bracketed fields, where your example has only two bracketed fields.

Wow, thanks so much for the quick response!

In case it's not obvious, I'm a bit of a music junkie.

Here is a better depiction of what I'm looking to do:

Original format:

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)
3 1 SOMEDAY WE’LL BE TOGETHER –•– Diana Ross and the Supremes (Motown)-9 (1)

Desired Format:



TW

|

LW

|

TITLE

|

Artist

|

(Label)

|

Weeks on Chart

|

(Peak To Date)

|

Weeks @ #1

|

  • | - | - | - | - | - | - | - |


    1

    |

    3

    |

    RAINDROPS KEEP FALLIN’ ON MY HEAD

    |

    B.J. Thomas

    |

    (Scepter)

    |

    10

    |

    (1)

    |

    (1 week at #1)

    |


    2

    |

    2

    |

    LEAVING ON A JET PLANE

    |

    Peter, Paul and Mary

    |

    (Warner Brothers)

    |

    11

    |

    (1)

    |




    |


    3

    |

    1

    |

    SOMEDAY WE’LL BE TOGETHER

    |

    Diana Ross and the Supremes

    |

    (Motown)

    |

    9

    |

    (1)

    |




    |

So far, I've had a little luck in separating out one column using that funky delimiter between the Song Title and Artist, but it's not fullproof.

If it makes a difference in ease, I don't really need that last column (Weeks @ # 1), but since it comes before the (Peak to Date), I thought it would be easier to include. I don't know if my reasoning is accurate.
Also, it doesn't matter if the parentheses around the column titles are retained.

I'd really appreciate any help - I'd be cutting and pasting for the rest of my years if I had to do it that way.

Thanks again,

Wendy

Thanks, so just to clarify:

  • in the output, weeks @ #1 is the last column,
  • but in the input, it's either missing or the penultimate item in parentheses

i.e.

One thing we need to do it is to work out whether (see examples 2, 3), the seventh incoming field is a (Peak to Date) or a Weeks @ #1 ?

Presumably the presence of a # character can be relied on as a clue to resolve that ?

Meanwhile, assuming that the pattern of the rows is more or less consistent, the theory of this first sketch is that:

  • if you have copied some rows of the the –•– source format
  • and run this macro

it should (if all goes well) repopulate the clipboard with a tab-delimited version of the rows, which you can then paste into something like Excel or Numbers.

chart rows parse test.kmmacros (33.7 KB)

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

    ObjC.import('AppKit');

    // Draft 0.2

    // main :: IO ()
    const main = () => {

        // TW LW TITLE –•– Artist (Label) (Weeks on Chart) (Peak To Date)
        // const xs = [
        //     '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)',
        //     '3 1 SOMEDAY WE’LL BE TOGETHER –•– Diana Ross and the Supremes (Motown)-9 (1)'
        // ];

        // const test = `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)
        // 3 1 SOMEDAY WE’LL BE TOGETHER –•– Diana Ross and the Supremes (Motown)-9 (1)`;

        return either(
            msg => alert('Parsing chart rows')(msg)
        )(
            tabbedLines => tabbedLines
        )(
            bindLR(
                clipTextLR()
            )(
                clip => clip.includes('–•–') ? (() => {
                    const rowParse = parse(chartListRow());
                    return Right(
                        lines(clip)
                        .flatMap(rowParse)
                        .map(
                            pair => fst(pair).join('\t')
                        ).join('\n')
                    );
                })() : Left('Clipboard contains no –•– lines.')
            )
        );
    };

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

    // chartListRow :: Parser [String]
    const chartListRow = () => {
        const
            integer = unsignedIntP(),
            saucer = string('–•–'),
            parenthesized = between(
                char('(')
            )(
                char(')')
            )(
                many(satisfy(c => c !== ')'))
            ),
            peak = token(
                between(
                    char('(')
                )(
                    char(')')
                )(integer)
            ),
            stringFromChars = cs => concat(cs).trim(),
            asWord = fmapP(concat);

        return bindP(
            integer
        )(tw => bindP(
            integer
        )(lw => bindP(
            manyTill(item())(saucer)
        )(title => bindP(
            some(satisfy(c => '(' !== c))
        )(artist => bindP(
            parenthesized
        )(label => bindP(
            char('-')
        )(_ => bindP(
            integer
        )(weeksInChart => bindP(
            altP(sequenceP([peak]))(
                sequenceP([
                    asWord(parenthesized),
                    asWord(peak)
                ])
            )
        )(onePeak => {
            const peakOne = reverse(onePeak);
            return pureP(
                [
                    tw,
                    lw,
                    title,
                    artist,
                    label,
                    weeksInChart
                ]
                .map(stringFromChars)
                .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))
    );

    // --------------------- 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];


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


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


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

Hi,

Ok, I think I see what you're asking

Not all of the titles would have that column, (1 week at # 1). When it is present, it is the 7th column of data. I assumed the column would have to be processed, so I left it in. But it makes little difference to me where it falls in the output. In fact, if it's easier it could be left out as since I am putting it into a spreadsheet I could figure that part out with a formula. I've just never run across this many possible 'delimiters' to try to split. I took some programming classes years ago, but my skills are very limited.

Output options:

Thanks again,

Wendy

The source material is lines of text which you copy ?

Have you been able to test the macro above ?

My Gosh, yes, the macro works BEAUTIFULLY!!

I would like it to be able to read a text file, but I can copy and paste just as well. This is AWESOME! Do you have a tip jar anywhere???

Wendy

Kind thought – no that's fine :slight_smile:

Others will show you regular expression approaches, and it's possible that you might find those easier to adjust.

(I just happen to find that JS approach quite legible and easy to rearrange, but preferences certainly vary)

if you prefer to read a text file, you can bring text contents into a variable or clipboard with:

image

Ok, so it works with that text, but how do I get it to work with others. Other ones that I am copying and pasting aren't working. (Sorry to be such a dolt, but I am excited to be able to really get rolling on this project.

W.

Are you happy to give us bigger sample of a multiline text ? (by DM if you prefer).

Ideally, if you prefer to bring it in from a text file, then a sample of that format.

Then we can make you a version which pulls in a file.

(and check the line delimiters, and any variation in the row patterns)

Absolutely!

I am attaching a text I just copied out of a larger list. It's from 2 different years - but I don't think there is any deviation in the format. So would this text file be ok?

I am perfectly fine with doing the repeat copy and paste, but I have not done much work with the saving/switching clipboards in Keyboard Maestro. I am in the middle
of The Sparks "Field Guide", so I should be better equipped to handle that at some point! This is absolutely fascinating to me. All of it!

You have absolutely made my day so far!

(Attachment charts.txt is missing)

(Attachment charts.txt is missing)

Thanks !

( It looks as if you might need to zip that for the forum software to upload it )

When your charts.txt file has been uploaded as as zip file, we could test by choosing its path in this macro:

Chart rows from file path.kmmacros (35.2 KB)

Sorry about that.

This one works great until the last two columns. Sometimes they are right, but sometimes the last column is being split. When they are wrong, it looks like the numbers might be transposed. I have used a little javascript in the past for Acrobat forms, but this JXA looks intense.

I ran it and have attached the text file and the Numbers Spreadsheet in a zip file.

Thanks so much for helping me with this.

Wendy

Archive.zip (135 KB)

Thanks ! I'll take a look this afternoon.

Update

In the meanwhile, could you tell me the names of a couple of the songs for which the last two fields are not coming out in the pattern that you need ?

(and perhaps, if there are any, the names of a couple of songs for which the output already seems OK ?)

Could you tell me more about the Numbers file which you have included in the Zip above:

  • Does that represent the target patten which you are aiming for ?
  • Or the output which you are getting from the script (including some glitches in the last two fields) ?

(My impression is the latter, because no divergence is immediately showing up)

It would be helpful to have a few examples of where (and how) the output is differing from what is needed.


I think I may have seen the problem.

Once we've pinned down the pattern completely, we can adjust the parser to fit it.

Looking at:

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)

Are we saying that:

  • The 7th field of the input is:
    • sometimes WeeksAtOne
    • and sometimes PeakToDate
  • WeeksAtOne (if present) is:
    • sometimes a parenthesized string
      - e.g. (1 week at #1)
    • and possibly sometimes just a simple parenthesized number ?
      - (1) ?
  • and the way to determine which semantic field is represented by the 7th physical field, is to look ahead and see whether or not a non-empty eighth field exists ?

Here's the next draft to test, based on the hypothesis above : -)

Chart rows from file path C.kmmacros (35.3 KB)

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

    ObjC.import('AppKit');

    // Draft 0.4

    // 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 !== ')'))
            ),
            stringFromChars = cs => concat(cs).trim(),
            quoted = cs => `"${concat(cs).trim()}"`;

        return bindP(
            integer
        )(tw => bindP(
            integer
        )(lw => bindP(
            fmapP(quoted)(manyTill(item())(saucer))
        )(title => bindP(
            fmapP(quoted)(some(satisfy(c => '(' !== c)))
        )(artist => bindP(
            fmapP(quoted)(parenthesized)
        )(label => bindP(
            char('-')
        )(_ => bindP(
            integer
        )(weeksInChart => bindP(
            fmapP(reverse)(
                some(fmapP(concat)(token(parenthesized)))
            )
        )(peakOne => pureP([
                tw,
                lw,
                title,
                artist,
                label,
                weeksInChart
            ]
            .map(stringFromChars)
            .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];


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


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


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

Ok,
Almost there. It seems (and I've checked with several lists) that whenever there is one that has '(weeks at # 1) it skips the next line. If you look at Numbers file, this one skips over the second line (comparing it to the text file) . If it would be easier to take out the search for that (weeks at # 1) I think it would be perfect, so if that's easier I am fine with that.
By the way, can you recommend a good beginner's learning source on JXA? Would love to learn how to do this.

Archive 2.zip (54.7 KB)

Good. Sounds like progress – I'll take a look : -)

JavaScript for work-flow scripting ? well, the best at the moment is probably the first 9 chapters of Eloquent JavaScript, though it's really more focused on a style for browser-embedded JS.

(Perhaps someone should sit down to write something else)

In the meanwhile, there are references here:

[JXA-Cookbook Wiki](https://github.com/JXA-Cookbook/JXA-Cookbook/wiki)