How to Surround Quoted Words With Tags?

Hello @ComplexPoint, would it be possible to enhance the JS to include numbers too?

Nach Bestätigen des letzten Einstellwertes in Anzeige <x1/>1<x2/> wechselt die Anzeige automatisch auf Anzeige <x3/>2<x4/>:
Na bevestiging van de laatste instelwaarde in display 1, verandert het display automatisch in display 2:

The 'isolated' numbers should be surrounded by tags like this:

Na bevestiging van de laatste instelwaarde in display <x1/>1<x2/>, verandert het display automatisch in display <x3/>2<x4/>:

The tagging should only be applied to isolated numbers (surrounded by spaced or punctuation marks etc.) and not to codes like Product1, Temp2, Speed1 etc.

I'll take a look : -)

Am I getting this right:

  • We are flanking isolated target NL digit runs (or just isolated single digits ?) with the same tags that flank their equivalents in source DE ?

  • (and we can assume I, guess, that any numbers in the DE will occur in the same sequence in the NL ?)

Thanks!

Answers:

  • Yes
  • Yes

And just to check, there will sometimes be multidigit numbers (10, 99, 100 etc) ?

Yes, there will. And numbers in the format:

100.000,00
14,4
1.1

(100.000,00 €, 14,4 ㏁, Kapitel 1.1)

Heavens ... that’s a bit more ambitious :slight_smile:

the currency names or symbols would be enclosed in the tags, or just the number string itself ?

Here's a first sketch, assuming a source string variable with the name strDE and a target string variable with the name strNL.

What we probably need now is some more examples of (input, output) pairs, and indications of where the output differs from expectation.

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

    // Draft 008

    // Adding a first pass to match the tagging of 
    // any numbers in the source.

    // main :: IO ()
    const main = () => {
        const
            kme = Application('Keyboard Maestro Engine'),
            kmVar = k => kme.getvariable(k),
            strSrc = kmVar('strDE'),
            strTgt = kmVar('strNL');

        const delims = '\"\'\“\”\‘\’{}()';

        // delimSplit :: String -> [String]
        const delimSplit = s =>
            groupBy(on(eq)(c => delims.includes(c)))(s);

        return tagMap(delimSplit)(
            src => tgt => src.includes('<') ? (
                `${firstTag(src)}${tgt}${lastTag(src)}`
            ) : delims.includes(tgt) ? (
                src
            ) : tgt
        )(
            strSrc
        )(
            // Numbers pre-tagged.
            tagMap(numberSplit)(
                src => tgt => isNumeric(src) ? (
                    tgt
                ) : `${firstTag(src)}${tgt}${lastTag(src)}`
            )(strSrc)(strTgt)
        );
    };


    // --------------------- TAGGING ---------------------

    // firstTag :: String -> String
    const firstTag = s =>
        s.startsWith('<') ? (() => {
            const i = [...s].findIndex(c => '>' === c);
            return -1 !== i ? (
                s.slice(0, 1 + i)
            ) : '';
        })() : '';


    // isNumeric :: String -> Bool
    const isNumeric = s =>
        /\b[\d\.,]*\d/.test(s);


    // lastTag :: String -> String
    const lastTag = s =>
        s.endsWith('>') && s !== firstTag(s) ? (() => {
            const
                i = [...s].reverse().findIndex(
                    c => '<' === c
                );
            return -1 !== i ? (
                s.slice(s.length - (1 + i))
            ) : '';
        })() : ''


    // numberSplit :: String -> [String]
    const numberSplit = s =>
        groupSplit(/\b[\d\.,]*\d/)(s);


    // tagMap :: (String -> [String]) -> 
    // (String -> String -> String) -> 
    // String -> String -> String
    const tagMap = fSplit =>
        fTag => src => tgt => zipWithLong(
            src => tgt => isNumeric(src) ? (
                tgt
            ) : `${firstTag(src)}${tgt}${lastTag(src)}`
        )(
            fSplit(src)
        )(
            fSplit(tgt)
        ).join('');


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

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


    // groupBy :: (a -> a -> Bool) -> [a] -> [[a]]
    const groupBy = fEq =>
        // Typical usage: groupBy(on(eq)(f), xs)
        xs => (ys => 0 < ys.length ? (() => {
            const
                tpl = ys.slice(1).reduce(
                    (gw, x) => {
                        const
                            gps = gw[0],
                            wkg = gw[1];
                        return fEq(wkg[0])(x) ? (
                            Tuple(gps)(wkg.concat([x]))
                        ) : Tuple(gps.concat([wkg]))([x]);
                    },
                    Tuple([])([ys[0]])
                ),
                v = tpl[0].concat([tpl[1]]);
            return 'string' !== typeof xs ? (
                v
            ) : v.map(x => x.join(''));
        })() : [])(list(xs));


    // groupSplit :: Regex -> String -> [String]
    const groupSplit = rgx =>
        // Lossless splitting on a pattern, with
        // pattern matches and remaining strings 
        // both retained.
        s => {
            const rgxg = new RegExp(rgx, 'g');
            return zipWithLong(
                a => b => [a, b]
            )(
                s.split(rgxg)
            )(
                s.match(rgxg)
            ).flat();
        };


    // on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
    const on = f =>
        // e.g. groupBy(on(eq)(length))
        g => a => b => f(g(a))(g(b));


    // eq (==) :: Eq a => a -> a -> Bool
    const eq = a =>
        // True when a and b are equivalent in the terms
        // defined below for their shared data type.
        b => {
            const t = typeof a;
            return t !== typeof b ? (
                false
            ) : 'object' !== t ? (
                'function' !== t ? (
                    a === b
                ) : a.toString() === b.toString()
            ) : (() => {
                const kvs = Object.entries(a);
                return kvs.length !== Object.keys(b).length ? (
                    false
                ) : kvs.every(([k, v]) => eq(v)(b[k]));
            })();
        };


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


    // zipWithLong :: (a -> a -> a) -> [a] -> [a] -> [a]
    const zipWithLong = f => {
        // A list with the length of the *longer* of 
        // xs and ys, defined by zipping with a
        // custom function, rather than with the
        // default tuple constructor.
        // Any unpaired values, where list lengths differ,
        // are simply appended.
        const go = xs =>
            ys => 0 < xs.length ? (
                0 < ys.length ? (
                    [f(xs[0])(ys[0])].concat(
                        go(xs.slice(1))(ys.slice(1))
                    )
                ) : xs
            ) : ys
        return go;
    };

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

Note that this version would leave any currency names or symbols outside the tags, with the strictly numeric strings inside the tags.

I suspect that my Macro that uses RegEx would properly handle digits as well as letters, but I can't be sure unless you post a compete example of real-world data showing:

  • Source data before any changes
  • Revised final data that you want to get
  • Clear rules
  • Comprehensive examples that will cover all use cases.

This is the same requirement for any problem where you want to process text.

This will give you the best results in the shortest time.