How to Create a Job Calculator?

For my translation jobs I receive analyses in XML format:

analysis.xml.zip (897 Bytes)

I would like to create a simple calculator like this:

example

The values in the second column are derived from the analysis. The values in the third column are preset, but should be editable.

What is the best approach to tackle this task?

example.xlsx.zip (7.6 KB)

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

See jq

[jq]( https://stedolan.github.io/jq/ )

and https://jqplay.org

Thank you @ComplexPoint!

What would be a good viewer/presentation?

If it's just a tabular report,
then perhaps a monospaced display in window is enough ?

1 Like

I want to be able to modify the Rate column.

Pending more ambitious (HTML form) GUI, you could always have 11 KM variables holding rates,

local_PerfectMatchRate
local_ContextMatchRate
etc

and display / update them in a KM Prompt for User input action, before generating and displaying the report.

1 Like

Hey Hans,

For simplicity I'd probably either use BBEdit, Numbers, or Microsoft Excel. (Numbers, and BBEdit Lite are free.)

All are scriptable.

To do an editable Custom HTML Prompt with a table would be quite doable, but the learning curve is high.

-Chris

1 Like

I have created an HTML page with Seamonkey. I'm not sure if that is enough for a macro or that I should use something more complex (too complex for me at the time) with CSS:

example
(The values in the white table don't match those in analysis_new.xml in the ZIP.)

If the HTML page is enough for a macro, I'd be grateful if someone could show me how to link the words variable for the Perfect Matches (I can do the other ones myself).

Perfect Match Rate should be preset with 0,00 but also be an editable field. How to achieve that?

The names of the variables are as follows:

Perfect Matches: %JSONValue%local_analyse[1].attributes.words%
Context Matches: %JSONValue%local_analyse[2].attributes.words%
Repetitions: %JSONValue%local_analyse[6].attributes.words%
CrossFileReps: %JSONValue%local_analyse[5].attributes.words%
Locked: %JSONValue%local_analyse[4].attributes.words%
100%: %JSONValue%local_analyse[3].attributes.words%
95%-99%: %JSONValue%local_analyse[14].attributes.words%
85%-94%: %JSONValue%local_analyse[13].attributes.words%
75%-84%: %JSONValue%local_analyse[12].attributes.words%
50%-74%: %JSONValue%local_analyse[11].attributes.words%
New/AT: %JSONValue%local_analyse[8].attributes.words%
Total: %JSONValue%local_analyse[7].attributes.words%

Job Calculator - components.zip (6.0 KB)

1 Like

Let me first try to make my first HTML macro.

Have at it!

:sunglasses:

1 Like

New approach: I've taken the example macro from the wiki and adapted it to my needs.

Question: How can I get the variable %JSONValue%local_analyse[1].attributes.words% in cell B2?

JobCalculator.zip (20.8 KB)

The general approach, Hans, would be to have a JavaScript function that would be called when the value of any cell in the Rate column changes. The function would multiply the Words (probably delivered as an argument to the function) by the Rate and display that total in the Amount column.

Similarly any change in the Amount column would trigger another JavaScript function to display a new grand total.

These two functions would sit in the HTML header and be called by an onchange event within the HTML table's cells.

That's just a sketch. I'll see if I can put together a small demo of it tomorrow if it makes sense to you.

2 Likes

So here's Step 1 (everything but the JavaScript).

ss-599

I've rewritten your CSS and HTML, redesigning the form. Just a few point to note:

  • Everything is contained in one table (including the buttons)

  • Formatting is confined to the CSS for the most part

  • Colors revised to monochrome except the buttons which on hover indicate function

  • Text is aligned in the cells according to function

  • Cancel/Save buttons are ordered in the standard format

  • Added a Calculate button (perhaps temporarily for testing)

  • Just doing two rows for testing

  • Eliminated the jQuery and bootstrap imports (which were unused)

I've assumed (perhaps incorrectly) that you've already figured out how to get your Word counts from the XML into the form. So these are just static numbers.

I also note you use European decimal notation. I'm not, but I don't think it matters. You can try this form to see.

It's midday here and the middle of the night where you are, I suspect. I'll try to get the JavaScript cooking tonight.

Job Calculator.kmmacros (4.8 KB)

Keyboard Maestro Export

3 Likes

OK, here's a first pass. It shows how to set variables for word counts that will be used in the form. But it doesn't pull them from your XML.

ss-600

When you enter the last rate, the form automatically calculates all the totals in the gray cells. So no need for a Calculate button.

Pressing Return resets the form, which is probably a bad idea. You can set the behavior of the Escape and Return keys in KMInit.

function KMInit(){
	window.addEventListener("keydown",function(e){
		if (e.key == 'Enter') {
			e.preventDefault();
		}
	});
}

Job Calculator.kmmacros (5.9 KB)

Keyboard Maestro Export

2 Likes

Hello Mike, thank you for your big effort. Much appreciated! :pray:t2:

I've assumed (perhaps incorrectly) that you've already figured out how to get your Word counts from the XML into the form. So these are just static numbers.

Yes, @ComplexPoint has been very kind and helped me with that step.

I'll try to add rows for all word categories and put the JSON variables in them. Now that both the XML parsing and the HTML is controlled by JavaScript, this should be doable for me (at least, I'll try :slight_smile: ).

Your form has a Save button. I cannot see where the Grand Total is saved.

I inherited that from the HTML form in your download, Hans. I didn't know what you wanted it to do so I didn't take it any further.

You might want to save the HTML as a file that you can open in a browser or you might want to save particular information from it.

As it is now, on the named local variables are accessible by Keyboard Maestro outside the prompt. If you wanted to save totalAmt, for example, you would have to convert it to something that looks like pmWords. Keyboard Maestro can save localpmWords.

Here's a little fancier version that:

  • Formats numbers with separators and two decimals where appropriate
  • Does not run asynchronously (missed that setting before)
  • Disables the Return key
  • Autofocuses on right column fields
  • Displays the total Amount when you click the Save button

ss-602

Job Calculator.kmmacros (8.3 KB)

2 Likes

Thank you @mrpasini! What should I do with the Custom HTML Prompt.kmactions?

I'll try to put everything together this weekend and will post back here.

Oops, sorry, I keep doing that (exporting just the action I was last working on instead of selecting the entire macro for export). I've fixed the upload (8.3K now) to include the whole macro.