sprintf.js icon indicating copy to clipboard operation
sprintf.js copied to clipboard

Expose regex or function to detect presence of placeholders

Open bbottema opened this issue 9 years ago • 6 comments

Hi guys, over at angular-logging, we heavily rely on sprintf to help us provide enhanced logging for angular's $log.

We now run into an issue where we would like to detect whether a string contains placeholders and how many. I see you have a separate regex for placeholders, but it is not exposed.

Would you guys consider either providing a function that returns a count of placeholders or alternatively exposing the placeholders regex? We need to know the number as we dynamically assign arguments to the sprintf invocation.

bbottema avatar May 15 '15 19:05 bbottema

At the moment I implemented a messy trick to count place holders:

var placeholderCount = 0;
var f = function() { return placeholderCount++ };
sprintf('this %s is %s a test %j', f,f,f,f,f,f,f,f,f,f,f,f,f,f,f,f,f,f,f,f,f,f,f,f,f,f,f,f);
// placeholderCount yields 3

This approach stops working, however, if sprintf ever implements a placeholder that is not compatible with numbers.

That and it is probably not the most efficient way to count placeholders.

You can see it in action with out logger in this jsFiddle.

bbottema avatar May 16 '15 21:05 bbottema

My guess is you're looking to implement a behaviour similar to that of console.log, where console.log("foo") yields foo console.log("foo", "bar") yields foo bar but console.log("%s") yields %s console.log("%s", "foo") returns foo and console.log("%s", "foo", "bar") results in foo bar

Now, the regular expression used for matching placeholders is utterly useless on it's own. If you look at the parser, you'll notice it consumes the string rather than simply doing a simple search and replace operation. It would be impossible otherwise to support the %% placeholder and arguments swapping. Thus, you would need a way to figure out whether the string contains placeholders and, most important, how many, so that you could subsequently call sprintf with a limited number of arguments and join its output with the rest of your arguments.

I'll think about it. Maybe I could make the sprintf.parse method (albeit undocumented) return information that would be useful to 3rd parties.

Meanwhile, as far as I can tell, console.log doesn't use a full-blown sprintf implementation. It seems, instead, to do simple replacements. So think about your use cases. Maybe a simpler method might fit your use cases.

alexei avatar May 17 '15 14:05 alexei

As the function argument doesn't work for named parameters (why not? A computed value can be an object too), I moved on to some simple regexes:

var hasNamedHolders = /\x25\([a-zA-Z0-9_]+\)[b-fijosuxX]/.test(args[0]);
var placeholderCount = args[0].match(/\x25(\d\$)?[b-fijosuxX]/g).length;

I rather rely on public API however.

bbottema avatar May 18 '15 19:05 bbottema

Like I said in my previous comment, you are looking to count the number of arguments needed by the format string, not the number of placeholders i.e. "%1$s %1$s %1$s" has 3 placeholders, but only requires one argument.

alexei avatar May 18 '15 20:05 alexei

Ahh yes, I see. The first work around I posted actually works for that then? I would test it but I'm not behind a pc currently. And with a separate check only for named arguments would rule out multiple arguments, or doesn't it?

If that's the case, then at least I have a functional workaround.

bbottema avatar May 18 '15 21:05 bbottema

I wrote a proof of concept for the work-around, predicated on the assumption that named placeholders limit the necessary arguments to one, otherwise even sprintf will fail saying you can't mix named and positioned placeholders:

I'm not too happy with it, but it seems to do the job for now. Am I missing anything important here?

function countSprintfHolders(pattern) {
    var hasNamedHolders = /\x25\([a-zA-Z0-9_]+\)[b-fijosuxX]/.test(pattern);
    if (hasNamedHolders) {
        return 1;
    }
    var placeholderCounter = 0;

    function f(index) {
        return function () {
            // keep track of highest arg index, needed for single -but indexed- placeholders placeholder (ie. %6$s consumes the first 6 arguments)
            placeholderCounter = Math.max(placeholderCounter, index);
        };
    }
    sprintf(pattern, f(1), f(2), f(3), f(4), f(5), f(6), f(7), f(8), f(9), f(10));
    return placeholderCounter;
};

jsFiddle with some tests

bbottema avatar May 19 '15 08:05 bbottema