# I Need Help Using Multiple Lines on the Clipboard

1.180 cm

3.009 cm

0.692 cm

0.645 cm

I have those values stored in my clipboard.

When I paste then I get that exact result posted. I get a blank line between each value.

I would like to store the individual values into individual variables, for example, from VAR1 to VARn from the smallest to the biggest value...

Can you help me with ideas?

Thanks

Something like this should work:

Collect numbers.kmmacros (2.4 KB)

It runs this Python script to pull out the values and put them into JSON form:

``````#!/usr/bin/python3

import re
import sys
import json

# Read standard input and save to string

# Collect the nonblank lines into a list
lines = re.split(r'\n+', inp.strip())

# Make a list of (number, unit) tuples
nums = []
for s in lines:
# Find the numeric part
match = re.match(r'[-.0-9]+', s)
if match:
# Break into number and unit
numPart = match.group(0)
unitPart = s[len(numPart):].strip()
# Add (number, unit) tuple to the end of list
nums.append((float(numPart), unitPart))

# Sort numerically
nums.sort()

# Convert to list of strings
values = [ f'{n} {u}' for n, u in nums ]

# Print in JSON form
print(json.dumps(values))
``````

It then uses the `Set Variables to JSON` action to create a set of variables named MYVAR1, MYVAR2, etc. These variables are strings that have both the number and the unit. They're sorted by the numeric part, so "1 cm" will come before "2 mm."

The Python script tries to handle variations in the input—missing or extra blank lines, missing or extra space between the number and the unit, lines with no numbers, lines with no units—in a reasonable way, but it's certainly not unbreakable.

Many times it is not that useful to store data in a bunch of numbered KM Variables.
It can be much more effective and easy to use to the KM For Each action with a Lines In collection, which can even be set to ignore blank lines.

So you could have something like this:

Of course, within the For Each Action, you can put whatever Actions you want to process each line.

Note that I use the KM Variable `Local__Line` which is a Local Variable, which is automatically deleted from the KM Variable list.

Questions?

and just in case a numbered and value-ordered sequence of KM variables really is what you need, here is a variant which uses an Execute JavaScript action:

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

ObjC.import('AppKit');

const kmVarNameStem = 'var';

// Numbered sequence of Keyboard Maestro variables
// created from any floatNumber unitString lines
// in the macOS system clipboard.

// main :: IO ()
const main = () =>
either(
// An explanatory message,
)(
// or a list of the KM variable names bound.
x => x
)(
bindLR(
clipTextLR()
)(
clip => bindLR(
sortedTuplesFromClipLR(clip)
)(
pairs => Right(
numberedKMvarsFromPairs(pairs)
)
)
)
);

// numberedKMvarsFromPairsLR :: [(Int, String)] ->
// IO [Keyboard Maestro Variable]
const numberedKMvarsFromPairs = pairs => {
const
kme = Application('Keyboard Maestro Engine'),
indexDigitCount = str(pairs.length).length;
return pairs.reduce(
(a, tuple, i) => {
const
k = kmVarNameStem + str(1 + i)
v = unwords(first(str)(tuple));
return (
kme.setvariable(k, {
to: v
}),
`\${a}\n\${k} -> \${v}`
);
},
''
);
};

// sortedTuplesFromClip :: String ->
// Either String [(Float, String)]
const sortedTuplesFromClipLR = clip => {
// Either a message or a sorted list
// of (Float, String) tuples.
const
pairs = lines(clip).flatMap(
text => {
const ws = words(text);
return isNaN(ws[0]) ? (
[]
) : [
[parseFloat(ws[0])].concat(
unwords(ws.slice(1))
)
]
}
);
return 0 < pairs.length ? (
Right(sortBy(comparing(fst))(pairs))
) : Left(
'No (number, unit) pairs seen in clipboard.'
);
};

// ---------------------- MACOS ----------------------

// alert :: String => String -> IO String
s => {
const sa = Object.assign(
Application('System Events'), {
});
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 ---------------------

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

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

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

// cons :: a -> [a] -> [a]
const cons = x =>
// A list constructed from the item x,
// followed by the existing list xs.
xs => [x].concat(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];

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

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

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

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

// str :: a -> String
const str = x =>
Array.isArray(x) && x.every(
v => ('string' === typeof v) && (1 === v.length)
) ? (
x.join('')
) : x.toString();

// unwords :: [String] -> String
const unwords = xs =>
// A space-separated string derived
// from a list of words.
xs.join(' ');

// words :: String -> [String]
const words = s =>
// List of space-delimited sub-strings.
s.split(/\s+/);

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