call method stringWithFormat: possible?

In an AppleScript Studio application, I’ve been trying to get a “call method “stringWithFormat:” of class “NSString” with parameters” working, but so far to no avail.

Using:

set s to call method "stringWithFormat:" of class "NSString" with parameters "a string"

works and returns “a string”. The following, however, fails:

set s to call method "stringWithFormat:" of class "NSString" with parameters {"a string and %s", "another"}

does not work. It puts up a dialog saying “variable s is not defined” and the log window reports:

*** -[NSMethodSignature getArgumentTypeAtIndex:]: index out of bounds
  • I know the right method is being targeted because it works in the first example
  • The problem appears to be with the fact that “call method” sees more than one parameter and calls getArgumentTypeAtIndex for each one to determine what the corresponding parameter from the script should be formatted/passed on as. The method is declared with one argument, followed by “…” (meaning variable number of arguments)".
  • It appears that the NSMethodSignature class for stringWithFormat: does not encode arguments beyond the first and thus an OOB error results

If the above is correct, it would seem that it is fundamentally impossible to use “call method” with any method that uses a variable number of parameters. Anybody disagree or know better?

If true, does anyone know of a workaround? Adding some additional Objective-C to my project is not an issue, but I’d like something general. I’ve already considered writing a number of “glue” methods with fixed number and type of arguments, but flexibility is reduced this way. (I was thinking of only allowing string type parameters and writing glue for variants with 1-4 %s in the format string.)

Model: Dual G5 (2GHz, 2G)
Browser: Safari 419.3
Operating System: Mac OS X (10.4)

Hi dolfs,

the problem with ‘stringWithFormat:’ and other variable argument methods is, that they need arguments (parameters) of a primitive (non cocoa) type in addition to the format string. It’s some kind of list, which is handled internally by stdarg(3).
That’s why your first example worked and the others will (probably) not. Seems there is no built in NSArray- or NSString-to-this wanted argument type conversion in the call method - unfortunately.

But I think you could also use a ‘do shell script’ using printf in most cases, to get nice formatted strings:

set exampleString to (do shell script "printf \"%-7s string and %+12s\" \"one\" \"another\"")
set exampleString2 to (do shell script "printf \"one number: %04d and another: %f\" 5 23.073")

D.

Yes, and I have a shell script based approach as a backup. Being a programmer since the late 70’s, a Unix guru then, and a Mac programmer since 1984, I know the overhead of creating two new processes for every string I want to localize. It is just crazy overhead that any decent program could live without.
So, I want(ed) a better solution. The lack of support for “localized string with format” (where “localized string”, of course, is supported, is unbelievable and is probably a major reason why many ASS applications are not localized, or localizable.

This is not correct. “call method” is an interface to not Cocoa, but Objective-C, and even C for that matter. It has full support for calling method with non-Cocoa types. As long as the method has a fixed number of parameters you can call methods that expect and “int” with an AS variable (which, by definition, has no type per se). The implementation of “call method” uses the native language’s (Objective-C in this case) features to “discover” where the right method lives, and what kind of arguments it expects. In Java this process would be called “reflection”, but I am not sure what Objective-C folks call it.

The problem lies in the fact that apparently “call method” cannot handle the “translation” to a method that has a … (vararg) parameter. I am convinced that it is possible, however, to build support for that. It’d be best if Apple did it, as part of the framework.

In the mean time, I’ll implement a few aux methods in Objective-C to be used like this:

call method "stringWithFormat:param1" with parameters {fmt, str1}
call method "stringWithFormat:param1:param2" with parameters {fmt, str1, str2}
call method "stringWithFormat:param1:param2:param2" with parameters {fmt, str1, str2, str3}

The underelying native implementation must have pre-declared argument types, and so, the keep things easy, I’ll do this only for strings. You can then do more string formatting in AppleScript. I think with supporting up to 4 parameters one can cover most normal cases.

I’ll post code when done.

dolfs,

sorry, you are absolutely correct about cocoa - my mistake - was a little late here when I posted this yesterday. But ‘even C’? How should that work (if not wrapped inside some Objective-C?)

And regarding the primitive types - are you sure about this? I always thought, AppleScript does not work with primitives internally: I believed, a string was always an NSMutableString and an integer/real/number … was an NSNumber (that’s why it can be coerced so easily)?

Nice idea. What about writing only one aux method like this:

call method "stringWithFormat:params:" with parameters {fmt, {param1,param2 ... paramn}}

and switch it internally:

-(NSString *)stringWithFormat:(NSString *)fmt params:(NSArray *)params{
switch ([params count]) {
case 0:
return fmt;
break;
case 1:
return ([NSString stringWithFormat:fmt, [params objectAtIndex:0]]);
break;
case 2:
...
default:
// too many arguments
return nil;
break;
}
}

D.

I believe they call it Introspection.

-sD-
Dr. Scotty Delicious, Scientist.

OK. I’ve got the basics working. Code below.
Keep in mind that, as mentioned, all AS expressions come across as follows:
string → NSCFString
list → NSArray
record → NSCFDictionary
number → NSCFNumber
date → NSCFDate
In other words, they all arrive in the Objective-C code as objects. Consequently, the only format code you can use with them is %@ which essentially gives you a “primitive” string description of the object in question.
The code below needs to be added to your project as a .h and .m file respectively. It uses a category to expand the NSString class with a new method, that you will call like this:

on stringWithFormat(fmt, params)
    call method "stringWithFormat:andParams:" of class "NSString" with parameters {fmt, params as list}
end stringWithFormat

The “as list” construct is for you convenience so that if you have only a single parameter, you do not have to add the additional {} (unless it is a record, in which case you do want to add them!).
The implementation, as given here, handles up to 4 parameters, but you can expand this arbitrarily in a trivial manner.

To make the functionality more useful, I am considering scanning the format string and recognizing other formats, such as %d, %s etc and converting the incoming parameters to the right format before passing them on. More on that later. This code has been tested and is working!

NSStringASS.h:

[code]//
// NSStringASS.h
// Photo Utils
//
// Created by Dolf Starreveld on 1/16/07.
// Copyright 2007 Starfield Consulting. All rights reserved.
//

#import <Cocoa/Cocoa.h>

@interface NSString (ASS)

  • (NSString *) stringWithFormat: (NSString *) fmt andParams: (NSArray *) params;

@end[/code]
NSStringASS.m

[code]//
// NSStringASS.m
// Photo Utils
//
// Created by Dolf Starreveld on 1/16/07.
// Copyright 2007 Starfield Consulting. All rights reserved.
//

#import “NSStringASS.h”

@implementation NSString (ASS)

  • (NSString *) stringWithFormat: (NSString *) fmt andParams: (NSArray *) params
    {
    switch ([params count]) {
    case 0:
    return fmt;
    break;
    case 1:
    return ([NSString stringWithFormat:fmt, [params objectAtIndex:0]]);
    break;
    case 2:
    return ([NSString stringWithFormat:fmt, [params objectAtIndex:0],
    [params objectAtIndex:1]]);
    break;
    case 3:
    return ([NSString stringWithFormat:fmt, [params objectAtIndex:0],
    [params objectAtIndex:1],
    [params objectAtIndex:2]]);
    break;
    case 4:
    return ([NSString stringWithFormat:fmt, [params objectAtIndex:0],
    [params objectAtIndex:1],
    [params objectAtIndex:2],
    [params objectAtIndex:3]]);
    break;
    default:
    NSLog(@“stringWithFormat:andParams: called with more then 4 arguments”); // too many arguments
    return nil;
    }
    }

@end[/code]

For those who care, I have expanded the code to handle the generic case. An example that I ran:

stringWithFormat("%-20s int %06d %8X %9.3f", {"Formatted:", 123, 1023, 132.1235})

it produced this output

"Formatted:___________int 000123______3FF___132.124"

Notes:

  • To make sure that spaces that were output are shown correctly here, they were replaced with underscores
  • Notice the string was left aligned in 20 positions
  • Notice correct left zero padding of the int value
  • Notice that displaying only three decimals in the floating value causes it to be rounded (correct)
  • When numbers are passed that are internally converted to the format indicated by the current format specifier. If the actual number is too big, it may lead to unexpected results (although predictable). Not shown here.

Positional parameters (using codes like %3$d) are available as well.

The code, while not large, is too large to post in a message. If you need it/want it, drop me a line.