Best Way to Solve this Simple Concatenate Challenge?

Greetings.

I have 10 device types.

DeviceType101 = AAA
DeviceType102 = BBB
DeviceType103 = CCC
DeviceType104 = DDD
DeviceType105 = EEE
DeviceType106 = FFF
DeviceType107 = GGG
DeviceType108 = HHH
DeviceType109 = III
DeviceType110 = JJJ

I need to put all of them on one line and rotate through each one as the starting one.
Like this:

AAA BBB CCC DDD EEE FFF GGG HHH III JJJ
BBB CCC DDD EEE FFF GGG HHH III JJJ AAA
CCC DDD EEE FFF GGG HHH III JJJ AAA BBB
DDD EEE FFF GGG HHH III JJJ AAA BBB CCC
EEE FFF GGG HHH III JJJ AAA BBB CCC DDD
FFF GGG HHH III JJJ AAA BBB CCC DDD EEE
GGG HHH III JJJ AAA BBB CCC DDD EEE FFF
HHH III JJJ AAA BBB CCC DDD EEE FFF GGG
III JJJ AAA BBB CCC DDD EEE FFF GGG HHH
JJJ AAA BBB CCC DDD EEE FFF GGG HHH III

The DeviceTypes will change often, and I can easily type in different values, like MMM, NNN, or OOO every day. But the number of items may change from 8, 9, 10, 11, maybe 12.

I could just manually type in 10 concatenate commands, but since the number of devices may change, this will be a nuisance to update.

It seems like loops would be the way to go. I set up a loop but can’t get the dynamic variable to work.

nnn = 100
repeat 10 times

  • nnn = nnn + 1
  • DeviceList101 = DeviceList101 + DeviceType%nnn%

Is there a better way to do this? or how do I get the dynamic variable to work?

I’ve played with the percent characters for an hour now and nothing works. Do I need tokens or something?

This kind of thing ?

( Composition of generic functions pasted into an Execute JavaScript for Automation action )

(Source code below)

Device rotations.kmmacros (20.4 KB)

(() => {
    'use strict';


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

    // concat :: [[a]] -> [a]
    // concat :: [String] -> String
    const concat = xs =>
        xs.length > 0 ? (() => {
            const unit = typeof xs[0] === 'string' ? '' : [];
            return unit.concat.apply(unit, xs);
        })() : [];

    // drop :: Int -> [a] -> [a]
    // drop :: Int -> String -> String
    const drop = (n, xs) => xs.slice(n);

    // enumFromTo :: Int -> Int -> [Int]
    const enumFromTo = (m, n) =>
        Array.from({
            length: Math.floor(n - m) + 1
        }, (_, i) => m + i);

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

    // map :: (a -> b) -> [a] -> [b]
    const map = (f, xs) => xs.map(f);

    // replicate :: Int -> a -> [a]
    const replicate = (n, x) =>
        Array.from({
            length: n
        }, () => x);

    // rotate :: Int -> [a] -> [a]
    const rotate = (n, xs) => {
        const lng = xs.length;
        return lng > 0 ? takeDropCycle(lng, n, xs) : [];
    };

    // take :: Int -> [a] -> [a]
    const take = (n, xs) => xs.slice(0, n);

    // take N Members of an infinite cycle of xs, starting from index I
    // takeDropCycle :: Int -> [a] -> [a]
    const takeDropCycle = (n, i, xs) => {
        const
            lng = xs.length,
            m = n + i;
        return drop(i,
            take(m,
                (lng >= m ? xs : concat(replicate(Math.ceil(m / lng), xs)))
            )
        );
    };

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

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

    // ROTATED DEVICES -------------------------------------------------------

    // xs :: [String]
    const xs = map(
        x => x.split(' = ')[1],
        lines(Application("Keyboard Maestro Engine")
            .getvariable('deviceList'))
    );

    // String
    return unlines(
        map(
            n => unwords(rotate(n, xs)),
            enumFromTo(0, xs.length - 1)
        )
    );
})();

Applescript needs a little more help (more generic functions to paste), but can still do it in just the same way, if you prefer an Execute Applescript action in KM

-- GENERIC FUNCTIONS -----------------------------------------------------

-- ceiling :: Num -> Int
on ceiling(x)
    set {n, r} to properFraction(x)
    if r > 0 then
        n + 1
    else
        n
    end if
end ceiling

-- concat :: [[a]] -> [a]
-- concat :: [String] -> String
on concat(xs)
    if length of xs > 0 and class of (item 1 of xs) is string then
        set acc to ""
    else
        set acc to {}
    end if
    repeat with i from 1 to length of xs
        set acc to acc & item i of xs
    end repeat
    acc
end concat

-- drop :: Int -> [a] -> [a]
on drop(n, a)
    if n < length of a then
        if class of a is text then
            text (n + 1) thru -1 of a
        else
            items (n + 1) thru -1 of a
        end if
    else
        {}
    end if
end drop

-- enumFromTo :: Int -> Int -> [Int]
on enumFromTo(m, n)
    if m > n then
        set d to -1
    else
        set d to 1
    end if
    set lst to {}
    repeat with i from m to n by d
        set end of lst to i
    end repeat
    return lst
end enumFromTo

-- intercalate :: String -> [String] -> String
on intercalate(s, xs)
    set {dlm, my text item delimiters} to {my text item delimiters, s}
    set str to xs as text
    set my text item delimiters to dlm
    return str
end intercalate

-- Splits a string on linefeed Chars
-- lines :: String -> [String]
on |lines|(xs)
    splitOn(linefeed, xs)
end |lines|

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

-- 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

-- properFraction :: Real -> (Int, Real)
on properFraction(n)
    set i to (n div 1)
    {i, n - i}
end properFraction

-- Egyptian multiplication - progressively doubling a list, appending
-- stages of doubling to an accumulator where needed for binary 
-- assembly of a target length
-- replicate :: Int -> a -> [a]
on replicate(n, a)
    set out to {}
    if n < 1 then return out
    set dbl to {a}
    
    repeat while (n > 1)
        if (n mod 2) > 0 then set out to out & dbl
        set n to (n div 2)
        set dbl to (dbl & dbl)
    end repeat
    return out & dbl
end replicate

-- rotate :: Int -> [a] -> [a]
on rotate(n, xs)
    set lng to length of xs
    if lng > 0 then
        takeDropCycle(lng, n, xs)
    else
        {}
    end if
end rotate

-- splitOn :: String -> String -> [String]
on splitOn(strDelim, strMain)
    set {dlm, my text item delimiters} to {my text item delimiters, strDelim}
    set xs to text items of strMain
    set my text item delimiters to dlm
    return xs
end splitOn

-- take :: Int -> [a] -> [a]
on take(n, xs)
    if class of xs is string then
        if n > 0 then
            text 1 thru min(n, length of xs) of xs
        else
            ""
        end if
    else
        if n > 0 then
            items 1 thru min(n, length of xs) of xs
        else
            {}
        end if
    end if
end take

-- take N Members of an infinite cycle of xs, starting from index I
-- takeDropCycle :: Int -> [a] -> [a]
on takeDropCycle(n, i, xs)
    set lng to length of xs
    set m to n + i
    
    if lng ≥ m then
        set ys to xs
    else
        set ys to concat(replicate(ceiling(m / lng), xs))
    end if
    
    drop(i, take(m, ys))
end takeDropCycle

-- unlines :: [String] -> String
on unlines(xs)
    intercalate(linefeed, xs)
end unlines

-- unwords :: [String] -> String
on unwords(xs)
    intercalate(space, xs)
end unwords


-- ROTATED DEVICES -------------------------------------------------------
on run
    tell application "Keyboard Maestro Engine" to set ¬
        strLines to getvariable ("deviceList")
    
    script suffix
        on |λ|(x)
            item 2 of splitOn(" = ", x)
        end |λ|
    end script
    set xs to map(suffix, |lines|(strLines))
    
    script rotated
        on |λ|(n)
            unwords(rotate(n, xs))
        end |λ|
    end script
    unlines(map(rotated, enumFromTo(0, (length of xs) - 1)))
end run

Hey ComplexPoint,

Thanks for the answers!
I’ve never gone into the scripts, just stayed in KM.

Is there a way to do it all inside KM with the variable name and the ‘%’ symbols?

Maybe it’s time for me to learn how to call an external routine. I have a ton of coding experience, but very little in javascript. But I think this going to be a long and deep rabbit hole.

Yeah, I haven’t even seen that yet where you could run javascript inside of KM. That is excellent. Hmmm.

but very little in javascript

Reinventing the wheel, and doing the rotation manually, a more DIY version for pasting into a KM Execute Applescript action might be:

-- enumFromTo :: Int -> Int -> [Int]
on enumFromTo(m, n)
    if m > n then
        set d to -1
    else
        set d to 1
    end if
    set lst to {}
    repeat with i from m to n by d
        set end of lst to i
    end repeat
    return lst
end enumFromTo


-- intercalate :: String -> [String] -> String
on intercalate(s, xs)
    set {dlm, my text item delimiters} to {my text item delimiters, s}
    set str to xs as text
    set my text item delimiters to dlm
    return str
end intercalate

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

-- 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

-- splitOn :: String -> String -> [String]
on splitOn(strDelim, strMain)
    set {dlm, my text item delimiters} to {my text item delimiters, strDelim}
    set xs to text items of strMain
    set my text item delimiters to dlm
    return xs
end splitOn

-- unlines :: [String] -> String
on unlines(xs)
    intercalate(linefeed, xs)
end unlines

-- unwords :: [String] -> String
on unwords(xs)
    intercalate(space, xs)
end unwords

on run
    script suffix
        on |λ|(x)
            item 2 of splitOn(" = ", x)
        end |λ|
    end script
    tell application "Keyboard Maestro Engine" to set ¬
        xs to my map(suffix, paragraphs of (getvariable ("deviceList")))
    
    script rotated
        on |λ|(n)
            if n ≠ 1 then
                unwords(items n thru -1 of xs & items 1 thru (n - 1) of xs)
            else
                unwords(xs)
            end if
        end |λ|
    end script
    
    unlines(map(rotated, enumFromTo(1, length of xs)))
end run

Actually KM supports scripting in many different languages. The two most common are AppleScript and JavaScript for Automation (JXA) .

For more information, see Scripting (KM Wiki).

Of course, the KM Wiki shows you how to use scripts with KM, not how to write the scripts (except for access to KM Variables). But there are a lot of experienced scripters, like @ComplexPoint, @ccstone, and others who hang out in the KM forum. So, if you decided to try your hand scripting, and have questions, feel free to post here (in a new topic).

You can also learn a lot from scripts already posted here.
See topics with these tags:
Topics tagged with "applescript"
Topics tagged with "jxa"
Topics tagged with "javascript"
Topics tagged with "shellscript"

Good luck and have fun!

Well, certainly possible, and mileage will, of course, vary on which approach is quicker to assemble.

Device rotations without execute script action.kmmacros (24.2 KB)

1 Like

Hey ComplexPoint, I’ve spent many hours on this now, trying to understand everything you did, and learning things I didn’t know about KM. Your skill sets are amazing. You have opened new doors for me. Thank you so much.

1 Like

Pleasure. Fun exercise.

(My guess is that Peter might see a shorter route through it : -)

1 Like

Hey Cliff,

Just for fun…

------------------------------------------------------------------------------
# Auth: Christopher Stone
# dCre: 2017/11/05 14:54
# dMod: 2017/11/05 14:54 
# Appl: Vanilla AppleScript
# Task: Build a text array from a specified string.
# Libs: None
# Osax: None
# Tags: @Applescript, @Script, @Build, @Array, @String
------------------------------------------------------------------------------

set myInput to "
DeviceType101 = AAA
DeviceType102 = BBB
DeviceType103 = CCC
DeviceType104 = DDD
DeviceType105 = EEE
DeviceType106 = FFF
DeviceType107 = GGG
DeviceType108 = HHH
DeviceType109 = III
DeviceType110 = JJJ
"

set AppleScript's text item delimiters to {" = ", linefeed}
set theCodes to text items of myInput

repeat with i in theCodes
   if length of i ≠ 3 then
      set contents of i to 0
   end if
end repeat

set theCodes to (text of theCodes)
set lineLength to length of theCodes
set theCodes to theCodes & theCodes

set myArray to {}

set AppleScript's text item delimiters to space

repeat with i from 1 to lineLength
   set end of myArray to (items i thru (i + 9) of theCodes) as text
end repeat

set AppleScript's text item delimiters to linefeed

set myArray to myArray as text

------------------------------------------------------------------------------

When installed — the Satimage.osax adds a wonderful array of features to AppleScript.

This script uses the Satimage.osax’s regular expression support to easily find the 3-digit codes in the input text and is consequently more concise than the above script.

------------------------------------------------------------------------------
# Auth: Christopher Stone
# dCre: 2017/11/05 14:54
# dMod: 2017/11/05 14:59
# Appl: AppleScript + the Satimage.osax
# Task: Build a text array from a specified string (SIO Version).
# Libs: None
# Osax: Satimage.osax --> http://tinyurl.com/satimage-osaxen
# Tags: @Applescript, @Script, @Build, @Array, @String, @Satimage.osax
------------------------------------------------------------------------------

set myInput to "
DeviceType101 = AAA
DeviceType102 = BBB
DeviceType103 = CCC
DeviceType104 = DDD
DeviceType105 = EEE
DeviceType106 = FFF
DeviceType107 = GGG
DeviceType108 = HHH
DeviceType109 = III
DeviceType110 = JJJ
"

# Uses the Satimage.osax's regular expression support to easily find the codes:
set theCodes to fnd("(?-i)[A-Z]{3}", myInput, true, true) of me

set lineLength to length of theCodes
set theCodes to theCodes & theCodes

set myArray to {}

set AppleScript's text item delimiters to space

repeat with i from 1 to lineLength
   set end of myArray to (items i thru (i + 9) of theCodes) as text
end repeat

set AppleScript's text item delimiters to linefeed

set myArray to myArray as text

------------------------------------------------------------------------------
--» HANDLERS
------------------------------------------------------------------------------
on fnd(_find, _data, _all, strRslt)
   try
      find text _find in _data all occurrences _all string result strRslt with regexp without case sensitive
   on error
      return false
   end try
end fnd
------------------------------------------------------------------------------

-Chris

Thanks Chris. I’m still experimenting. This has turned into a learning experience. It feels like it’s time to learn how to add scripts into KM. I was busy all day today, and I’m back at if for a little while.

Satimage is interesting.

Hey Cliff,

You're welcome.

Keyboard Maestro has such a rich and deep feature set that this is bound to be perpetually true — it is for me at least, and I've been using it since early 2004.

The functionality the osax adds to AppleScript is very deep and very useful.

Anyone serious about AppleScript should also check out Script Debugger.

-Chris

Chris knows what I’m going to say :slight_smile:

AppleScript has given me years of fun, but now I get better libraries and better coverage (on iOS Drafts, 1Writer, OmniGraffle, etc, as well as web) from JavaScript.

(Also, you get map filter and reduce for free in JS, whereas you have to squeeze them out of AS, and Applescript records are a mild nightmare, compared to the much easier and less error-generating dictionaries in more or less every other scripting language)

So my personal feeling is that for anyone starting an investment in scripting now, Applescript is not looking like the obvious bet, these days … Omni, for example, are moving their scripting efforts to omniJS, essentially because of the elephant in the room ( iOS ).

The Applescript way:

set rec to {alpha:1, beta:2, gamma:3}

epsilon of rec

The Javascript way:

No error. Just a testable undefined value.

But, OK, lets avoid interruption by an AppleScript error. Let's first politely ask what keys the record has before we proceed.

The Applescript way:

use framework "Foundation"

set rec to {alpha:1, beta:2, gamma:3}

(current application's NSDictionary's dictionaryWithDictionary:rec)'s allKeys() as list

--> {"alpha", "beta", "gamma"}

The JavaScript way:

rec = {alpha:1, beta:2, gamma:3}

Object.keys(rec)

// -> ["alpha", "beta", "gamma"]

( It is often explained to me that AppleScript is 'English-like' and 'easier'. I always agree :slight_smile: )

2 Likes

Just for fun also...

In Swift:

#!/usr/bin/env xcrun swift

import Foundation

var argument = "AAA BBB CCC DDD EEE FFF GGG III JJJ"

#if swift(>=3.0)
	if let input = ProcessInfo.processInfo.environment["KMVAR_argument"] {
		argument = input
	}
#elseif swift(>=1.0)
	print("Unsupported version of Swift (<= 3.0) please update to Swift 3.0 or above")
	break
#endif

var deviceTypes: [String] = argument.split(separator: " ").map(String.init)

var output = ""

for _ in 0...deviceTypes.count - 1 {
	output += deviceTypes.reduce("", {"\($0)\($0 == "" ? "" : " ")\($1)"})
	output += "\n"
	let newLast = deviceTypes.remove(at: 0)
	deviceTypes.append(newLast)
}

print(output)

Here is the script file:
concatenate.zip (17.1 KB)

Unzip and place somewhere, changing the Script file: location accordingly.

Note that in the Swift script, the default value for the argument is there just to test the script, you only need to change the variable in the KM macro (Set Variable "argument") if the devices change.

edit: I forgot to mention that it works with whatever number of devices you have in the variable.

1 Like

Thanks Denis. This awesome. Nice and short in swift.

1 Like