Merging two variables line by line

Hi there, I was wondering if someone could help me with this. I have two variables each storing two separate lists.

the stored info in variable 1 is:

1,79,347,3
2,82,390,3
3,82,433,3
4,79,476,3
5,82,519,3
6,82,562,3
7,82,605,3
8,82,648,3

and variable 2 has:

74,211,1
74,227,1
74,243,1
74,259,1
74,275,1
74,291,1
74,307,1
74,323,1

The number of lines will always be the same, so in this case, there are 8 lines in each list. I was wondering how I would combine each line from variable 1 and variable 2 to make a new 3rd variable that would look like this

1,79,347,3,74,211,1
2,82,390,3,74,227,1
3,82,433,3,74,243,1
4,79,476,3,74,259,1
5,82,519,3,74,275,1
6,82,562,3,74,291,1
7,82,605,3,74,307,1
8,82,648,3,74,323,1

Thanks for the help!

Hi Billy,

A similar question was posed and answered here:

Take a look through that thread and see if you can't make one of the three possible solutions there work for you. If you run into any problems getting one of them to work, feel free to post again with further questions.

1 Like

beautiful, thank you, thats exactly it.

1 Like

and in JS, perhaps:

Columns from two variables.kmmacros (19.5 KB)

twoCols

(() => {
    'use strict';

    const main = () => {
        const
            kme = Application('Keyboard Maestro Engine'),
            strTwoCols = unlines(
                zipWith(
                    (x, y) => x + ',' + y,
                    lines(kme.getvariable('firstCol')),
                    lines(kme.getvariable('secondCol'))
                )
            );
            
        return (
            kme.setvariable('twoCols', {
                to: strTwoCols
            }),
            strTwoCols
        );
    };

    // GENERIC FUNCTIONS --------------------------------------

    // lines :: String -> [String]
    const lines = s => s.split(/[\r\n]/);

    // unlines :: [String] -> String
    const unlines = xs => xs.join('\n');

    // zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
    const zipWith = (f, xs, ys) =>
        Array.from({
            length: Math.min(xs.length, ys.length)
        }, (_, i) => f(xs[i], ys[i], i));

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

Interesting (to me):

The use case looks like restructuring a CSV file. Is it?

Which leads to my thought that some macros to manipulate CSV - such as deleting columns, inverting a CSV etc would be handy.

My personal first choice (for writing such things) would be the Python CSV reader module

(See, for example Trying to parse csv file into variables)

( I first met it when I had to process a 4Gb CSV file in an unexpected hurry – to my surprise it worked perfectly for that, and has always served well since )

Hey Billy,

Give this a try.

-Chris


Align Two Colums of Data v1.00.kmmacros (5.7 KB)

Tho even AppleScript could probably manage it :wink:

-- commaJoined :: String -> String -> String
on commaJoined(x, y)
    x & "," & y
end commaJoined

on run
    tell application "Keyboard Maestro Engine"
        set strFirst to getvariable ("firstCol")
        set strSecond to getvariable ("secondCol")
    end tell
    
    set strTwoCols to ¬
        unlines(zipWith(commaJoined, ¬
            paragraphs of strFirst, ¬
            paragraphs of strSecond))
end run


-- GENERIC REUSABLE FUNCTIONS --------------------------------------

-- min :: Ord a => a -> a -> a
on min(x, y)
    if y < x then
        y
    else
        x
    end if
end min

-- Lift 2nd class handler function into 1st class script wrapper
-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
    if class of f is script then
        f
    else
        script
            property |λ| : f
        end script
    end if
end mReturn

-- unlines :: [String] -> String
on unlines(xs)
    set {dlm, my text item delimiters} to ¬
        {my text item delimiters, linefeed}
    set str to xs as text
    set my text item delimiters to dlm
    str
end unlines

-- zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
on zipWith(f, xs, ys)
    set lng to min(length of xs, length of ys)
    if lng < 1 then return {}
    set lst to {}
    tell mReturn(f)
        repeat with i from 1 to lng
            set end of lst to |λ|(item i of xs, item i of ys)
        end repeat
        return lst
    end tell
end zipWith

Thanks. I have some experience with Python’s csv module. Choreographing it with KM would be the novel bit.

I’m wondering how to pass selected text into a Python module as a string - using KM. I should experiment.

So THIS got me there: https://stackoverflow.com/questions/29722041/passing-keyboardmaestros-variable-to-python-script#29722626

I just need to do the CSV stuff. Next stop is “put first column of CSV on clipboard back on clipboard” - as a simple example.

1 Like

I have a vague recollection (Peter will know) that there can be UTF8 limitations with reading KMVars through os.environ.

The other thing is, of course, to write out to a temporary file, and get Python to read that.

My JSA Action staples for those things include:

// filePath :: String -> FilePath
const filePath = s =>
    ObjC.unwrap(ObjC.wrap(s)
        .stringByStandardizingPath);

// getTemporaryDirectory :: IO FilePath
const getTemporaryDirectory = () =>
    ObjC.unwrap($.NSTemporaryDirectory());

// takeBaseName :: FilePath -> String
const takeBaseName = strPath =>
  strPath !== '' ? (
    strPath[strPath.length - 1] !== '/' ? (() => {
      const fn = strPath.split('/').slice(-1)[0];
      return fn.includes('.') ? (
        fn.split('.').slice(0, -1).join('.')
      ) : fn;
    })() : ''
  ) : '';

// takeExtension :: FilePath -> String
const takeExtension = strPath => {
    const
        xs = strPath.split('.'),
        lng = xs.length;
    return lng > 1 ? (
        '.' + xs[lng - 1]
    ) : '';
};

// takeFileName :: FilePath -> FilePath
const takeFileName = strPath =>
    strPath !== '' ? (
        strPath[strPath.length - 1] !== '/' ? (
            strPath.split('/')
            .slice(-1)[0]
        ) : ''
    ) : '';

// File name template -> temporary path
// (Random digit sequence inserted between template base and extension)
// tempFilePath :: String -> IO FilePath
const tempFilePath = template =>
    ObjC.unwrap($.NSTemporaryDirectory()) +
    takeBaseName(template) + Math.random()
    .toString()
    .substring(3) + takeExtension(template);

// File name template -> string data -> IO temporary path
// writeTempFile :: String -> String -> IO FilePath
const writeTempFile = (template, txt) => {
    const
        strPath = ObjC.unwrap($.NSTemporaryDirectory()) +
        takeBaseName(template) + Math.random()
        .toString()
        .substring(3) + takeExtension(template);
    return (writeFile(strPath, txt), strPath);
};

// writeFile :: FilePath -> String -> IO ()
const writeFile = (strPath, strText) =>
    $.NSString.alloc.initWithUTF8String(strText)
    .writeToFileAtomicallyEncodingError(
        $(strPath)
        .stringByStandardizingPath, false,
        $.NSUTF8StringEncoding, null
    );

So I have a macro - using inline Python - that prompts the user for space-separated column numbers and then selects those columns from CSV data on the clipboard. Today it prints the resulting CSV but I will modify to put the result back on the clipboard.

It’s pretty crappy Python - and others could do better - but it works well enough. And it uses the built-in Python csv class.

I’m not sure if it’s worth sharing. Perhaps I will and someone can work on it.

Anyway it’s a nice “proof of concept”, the concept being that you can manipulate CSV data with a KM macro. Which is actually VERY useful to me.

1 Like

Hey Martin,

That works.

Take careful note of this as well. The Execute a Shell Script action can provide STDIN from a number of sources.

This one uses the Clipboard:

Execute a Shell Script.kmactions (702 B)

Go ahead and post it.

We don't have much Python on the Forum, and some is better than none.

-Chris

Here's the macro I mentioned: “Select CSV Columns” Macro

Certainly I'll take improvement suggestions. But I'm likely to move onto the next CSV manipulation challenge soon.