How to Create a Job Calculator?

I would tend to think of it in two stages:

  1. XML -> JSON
  2. JSON querying and calculation

There are various approaches to stage one (web search should yield some options).

Stage two, generating a report from some JSON data can also be done in several ways:

  1. People like jq a lot
  2. You could do it with Keyboard Maestro %JSONValue% tags (see below), and some CALC functions.

(If you want to skip the JSON stage, XQuery is always worth experimenting with)


Reading from XML to JSON.kmmacros (11 KB)


Expand disclosure triangle to view JS source
(() => {
    "use strict";

    // unwrap :: NSObject -> a
    const unWrap = ObjC.unwrap;

    const main = () =>
        either(
            alert("JSON from XML")
        )(
            x => x
        )(
            bindLR(
                readFileLR(
					Application("Keyboard Maestro Engine")
                   .getvariable("xmlFilePath")
				 )
            )(
                compose(
                    fmapLR(
                        compose(
                            x => x.nest[0].nest[0].nest[0]
                            .nest.map(dict => dict.root),
                            xmlNodeDict
                        )
                    ),
                    xmlNodeFromXmlStringLR
                )
            )
        );

    // xmlNodeDict :: NSXMLNode -> Node Dict
    const xmlNodeDict = xmlNode => {
        const
            blnChiln = 0 < parseInt(
                xmlNode.childCount, 10
            );

        return Node({
            name: unWrap(xmlNode.name),
            content: blnChiln ? (
                undefined
            ) : (unWrap(xmlNode.stringValue) || " "),
            attributes: (() => {
                const attrs = unWrap(xmlNode.attributes);

                return Array.isArray(attrs) ? (
                    attrs.reduce(
                        (a, x) => Object.assign(a, {
                            [unWrap(x.name)]: unWrap(
                                x.stringValue
                            )
                        }),
                        {}
                    )
                ) : {};
            })()
        })(
            blnChiln ? (
                unWrap(xmlNode.children)
                .reduce(
                    (a, x) => a.concat(xmlNodeDict(x)),
                    []
                )
            ) : []
        );
    };


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


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

    // xmlNodeFromXmlStringLR :: XML String ->
    // Either String NSXMLNode
    const xmlNodeFromXmlStringLR = s => {
        const
            error = $(),
            node = $.NSXMLDocument.alloc
            .initWithXMLStringOptionsError(
                s, 0, error
            );

        return node.isNil() ? (() => {
            const
                problem = ObjC.unwrap(
                    error.localizedDescription
                );

            return Left(
                `Not parseable as XML:\n\n${problem}`
            );
        })() : Right(node);
    };

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

    // Left :: a -> Either a b
    const Left = x => ({
        type: "Either",
        Left: x
    });


    // Node :: a -> [Tree a] -> Tree a
    const Node = v =>
    // Constructor for a Tree node which connects a
    // value of some kind to a list of zero or
    // more child trees.
        xs => ({
            type: "Node",
            root: v,
            nest: xs || []
        });


    // Right :: b -> Either a b
    const Right = x => ({
        type: "Either",
        Right: x
    });


    // bindLR (>>=) :: Either a ->
    // (a -> Either b) -> Either b
    const bindLR = m =>
        mf => 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
        );


    // 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 => e.Left ? (
            fl(e.Left)
        ) : fr(e.Right);


    // fmapLR (<$>) :: (b -> c) -> Either a b -> Either a c
    const fmapLR = f =>
        // Either f mapped into the contents of any Right
        // value in e, or e unchanged if is a Left value.
        e => "Left" in e ? (
            e
        ) : Right(f(e.Right));

    // --------------------- LOGGING ---------------------

    // sj :: a -> String
    const sj = (...args) =>
    // 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.
        JSON.stringify.apply(
            null,
            1 < args.length && !isNaN(args[0]) ? [
                args[1], null, args[0]
            ] : [args[0], null, 2]
        );

    return sj(main());
})();
1 Like