An alternative approach to presetting a variable to zero, for counting calculations and such

Hi!

I recently realised that I allot of my macros could be simplified (by one action) with this alternative approach to presetting a variable to zero, all within a single Set Variable-action, and just thought I'd share it, as I am sure someone else might find it useful also.


Keyboard Maestro Export

Counting example in a Set Variable to Calculation action:
CALCULATE(0%Variable%local__counter% + 1)

Counting example in a in a Set Variable to Text action (for setting of variable arrays and such):
%Calculate%CALCULATE(0%Variable%local__counter% + 1)%

1 Like

Could you break this down for us? Or me, anyway. :laughing:

It looks as though you are setting the variable to 1 (not 0) as a side-effect of using the CALCULATE function. Is that right?

How is this any better than just setting a variable to 0 (or 1)? Edit: Oh, because you can run this action at any point in your macro, without having previously set a value. I think it better to be explicit in code/macros but I am curious about this.

Yes, sorry for the lacking explanation.

Any calculation set to myVar + 1 will fail if myVar is not already set to a numeric value (as + 1 in of itself is not a valid calculation). The usual way to solve this is by setting myVar to a value of "0" in an initial Set Variable action.

My above proposed approach instead solves this by treating myVar as text, padding it with a lead zero (the single 0 squeezed between the CALCULATE( and %Variable% above) before running the calculation. In this case the calculation will be ran as 0 + 1 in the case myVar is empty, and then ran as 01 + 1, 02 + 1, 03 + 1 … once it stores a numeric value, and in any case getting stored in myVar as a numeric value 1, 2, 3, …, without the lead zero.

I do not think intermediately padding the stored numeric value with a lead zero can bring with it any unwanted complications, so I think this one-action approach should be a fully good alternative to the usual two-action method

1 Like

That is brilliant. I love it. I wish I had come up with it. But then it's also predictable that some people would say "it's better to be clear and initialize properly." I guess to each his own. But thanks for the idea.

1 Like

Yes, I wrote in that spirit. My comment about initialising variables was about personal preference (I don't like to make things more opaque for myself!). Also, though, the forum is read by people with a variety of backgrounds and experience levels, so the difference between the boring old standard ways and something a bit fancy is, I think, worth noting.

While it's a neat trick, it does seem that you are replacing a single operation with something that has extra evaluations and will be repeated many times.

Inefficient execution in exchange for easier editing seems a poor swap, IMO.

I've got this as a Favourite action:

Scrawled in the margin: the humble Group action sometimes deserves more love. :upside_down_face:

1 Like

Counting calculations

For a series of integer index values, a natural KM idiom might be:

as in translating something like:

map sqrt [0..5]

to:

map sqrt [0..5].kmmacros (3.9 KB)


This idiom also allows, of course, for run-time derivation of the limits of the enumeration:

I guess you are right in that it is a little bit in the trick territory, as there are more intuitive ways of solving this task, but my main reason for wanting to use the demonstrated alternative approach, instead of the usual two action method, (with presetting a variable before the loop) is that it in my view makes the macro tidier — having to place a counter only where it is needed, not having to sprad it out at two locations.

Your macro group also seems to propose a method for a counter contained, and I am intrigued by it, but I am actually not able to understand nor recreate it. I suspect it is in the condition step I thread wrong, or I at least do not understand the Local_i < part ("less than" what?), so I’d really appreciate it if you could share your group and/or explain it’s workings.

I am not sure though about your claim that the alternative approach is more inefficient, at least not time consumption wise, as my testing have indicated that is is pretty much exactly as time consuming as the vanilla two action-method, and about one third faster than my old method of having a counter contained to a single position — a switch incrementing a variable if the variable is not empty, otherwise setting it to 0 (or 1 usually). Here’s the simple macro I used to time the different approaches:

Timing of counting methods.kmmacros (22 KB)

Macro Image

By copying the content of each of the groups into the repeat (in place og the #0A action now placed there), repeating the count 1000 times, I got the following results on my system:

Timing results

Method 0A: 2.003908s
Method 0B: 2.001838s
Method 1A: 2.002892s
Method 1B: 2.004936s
Method 2: 3.004981s

(Method 0A action within a group action 3.005136s)


Interestingly, as a bonus, while testing this timing I got results clearly indicating that containing an action within a Group actually consumes time (about 1ms) something I was not at all aware off.

Another interesting observation I made while doing the timings here is that the time consumption per action is suspiciously close to an average of 1ms pr. action/evaluation for all of these examples:

Repeat + Setting of variable: ~2ms
Repeat + Switch evaluation + Setting of variable: ~3ms
Repeat + Group + Setting of variable: ~3ms

Something that gives me a feeing that there might be a 1ms set minimum time consumption (or built in delay) per action

Not sure how this performs:

and execution speed is never a good proxy for reliability or ease of maintenance, but I do find KM For Each over a range of numbers quite easy to specify and maintain.

That was a compliment -- I like the way you're thinking!

It's a template. I'm old enough to use i (then j, k, etc) for my counter, so that's included. But the stop condition, the "less than what?", will vary each time so it has to be set in the macro. You could put in a placeholder like 10, but I'd rather have my macro fail to run with a "you forgot to set the condition, you muppet!" than do the wrong number of loops because I forgot to change the placeholder.

That's also why the < is left in there -- it forces an error unless the condition is edited, and it's usually the condition I want.

We're doing different things. You are repeating a set number of times, I'm repeating until a condition is met. You've extra ops in your incrementation action, I've an extra op every loop for the test. However...

As you say, something does seem to be "going on", and you can see it here -- there's similar runtime for all groups except "Explicit Range", which completes in 66% of the time of the others. And the major time sink in each group is a loop containing 2 actions -- except "Explicit", which is a loop containing 1 action.

Loop Test.kmmacros (15.6 KB)

Image

So I withdraw any concern I had about inefficient execution, especially in the day-to-day.

Ultimately, what you use depends on what you want to do and what you are comfortable with. @ComplexPoint's "Range" is fastest if you are incrementing by 1 -- you'll have to add extra actions to do eg. "every even number", which the other methods can do by simply changing the addition. Your method is great when the number of repeats is known at the start of the loop (or is an "Until" action, which tests the condition after at least one loop), the counter starts at 0, and the counter is positively incremented. But "initialise variable, do loop" is a good generic pattern for most other cases.

That was fun! Thanks, @Alexander, for making me think about this more.

1 Like

The For each set up as below performs the 1000 runs in 1.033929s on my system,[1] which is in of itself the fastest option, but as you write, speed is most of the time not necessarily the best merit to measure a solution by.

Image of testing setup

Keyboard Maestro Export

And you're right, For Each Number and it's 'built in' counter should definitely be considered allot of the time. For it’s counting abilities I’ve use often used For Each as an alternative to the Repeat, or as an alternative to For Each Line or Substring, etc.

For instance set up like this:

Keyboard Maestro Export

In my view the For Each's counting ability does not replace all other counting needs though[2] (as for instance for special conditions within an Until or While loop), so different standalone means of counting will many times still be useful.


  1. Interestingly once again suspiciously close to the '1ms pr. action execution time consumption' observed earlier ↩︎

  2. Unless using the For each to build all the other looping actions, something I think should be possible (all though oftentimes unnecessarily convoluted), by using a Break from loop, within an If then or Switch, within the loop ↩︎

Thank you! And sorry that my general insecurities here had me reading you in a less complimenting way.

Ah, of course, now I get it! And as I in many of my macros end up with counter1, counter2, etc. I do appreciate your counter naming scheme, and might adopt it myself!


Thank you for your Loop testing macro, and I get exactly the same kind of results as with my earlier test setup. It is a bit surprising to see how similarly these kind of varied methods perform. But it's good in that it gives room to pick the preferred method for each use case based on different parameters than speed.

Yes, it's very much "horses for courses". And my preference for "optimisation" is very misguided in most cases[1] -- modern machines are so fast and we have them doing so much other junk that saving an op or two in a KM macro won't make a difference.

Indeed, given that the biggest constraint on overall efficiency is probably us, it's worth being inefficient in the macro execution if that reduces our creating/maintaining load. As @ComplexPoint implied above, reliability and ease of maintenance are more important than saving a CPU cycle here or there.


  1. Obvious counter example -- processing a list of things one item at a time and repeatedly instantiating an AppleScript/shell environment, when you could pass the whole list in and process en bloc. ↩︎

1 Like

Nothing is slower than a road-side vehicle waiting for a pick-up truck :slight_smile: