05 May 2011

Delphi, Javascript and Floating Point Parameters

I recently came across an interesting little problem when using Delphi to call JavaScript in an HTML document loaded into a TWebBrowser control.

The code used the execScript method of the IHTMLWindow interface, which requires that a string containing the JavaScript function call is passed as one of its parameters.

So you have to assemble a string containing the function call and its parameters, something like this:

var
  JSFn: string;
  StrParam: string;
  IntParam: Integer;
  FloatParam: Double;
begin
  StrParam := 'Say \"Hello\"';  // double quotes must be escaped
  IntParam := 42;
  FloatParam := 1234.56789;
  JSFn := Format('Foo(%d, "%s", %.8f);', [IntParam, StrParam, FloatParam]);
  ExecJS(JSFn);
end;

I'll show ExecJS later. It's just a wrapper that ultimately calls IHTMLWindow.execScript with the required JavaScript.

The interesting bit is what happens to the floating point parameter. Delphi consults the current locale when formatting floats, so the decimal separator used in the resulting string depends of the locale. For example, on my UK English system JSFn has value:

Foo(42, "Say \"Hello\"", 1234.56789000);

whereas when I switch to a German locale the value changes to:

Foo(42, "Say \"Hello\"", 1234,56789000);

Now when the function call is passed to the JavaScript engine it has to interpret the string as valid JavaScript. What surprised me at first was the fact that, on the German locale, the float parameter was truncated to an integer value whilst on the UK locale it parsed correctly.

Once I thought about it more it became clear why. When the string containing the function call is passed to the JavaScript engine it has to be parsed. The string must contain valid JavaScript source code. And the JavaScript spec says that decimal numbers use '.' as the decimal separator, which means that any ',' separator is not valid. In fact ',' is the parameter separator so a parameter of '123,4567' is interpreted as two integer parameters: 123 and 4567.

Assume you have an HTML document that contains the following JavaScript in its <head> section:

<script type="text/JavaScript">
  function showNum(n1, n2) {
    alert(
      'n1 = ' + n1 + '\n\n' + 'n2 = ' + n2
    );
  }
</script>

You also have a Delphi program that loads the HTML document into a TWebBrowser control and has the following code attached to a button click:

ExecJS(
  Format('showNum(%.8f, %.8f)', [123.456, -42.56])
);

Running the Delphi program with a UK English locale will work as expected and you see the JavaScript alert box displaying this text:

n1 = 123.456
n2 = -42.56

which is what you expect. Switch to a German locale and run again and you'll get:

n1 = 123
n2 = 45600000

Luckily the fix is simple. We simply get information about the current decimal separator and change it to a '.'.

Here's my first attempt:

function JSFloat(const F: Double): string;
begin
  Result := FloatToStr(F);
  if DecimalSeparator <> '.' then
    Result := StringReplace(Result, DecimalSeparator, '.', [rfReplaceAll]);
end;

This uses the DecimalSeparator global variable that Delphi sets correctly for the current locale when the program starts.

When we change the code that calls the JavaScript function to the following, everything works no matter what the locale.

ExecJS(
  Format('showNum(%s, %s)', [JSFloat(123.456), JSFloat(-42.56)])
);

Using DecimalSeparator is not thread safe, and I think the code is rather clunky, so here's another, thread safe, version of JSFloat that I prefer:

function JSFloat(const F: Double): string;
var
  FS: TFormatSettings;
begin
  GetLocaleFormatSettings(GetThreadLocale, FS);
  FS.DecimalSeparator := '.';
  Result := FloatToStr(F, FS);
end;

This gets the format settings for the current thread's locale and then replaces the decimal separator with the required '.'. It uses the extended version of FloatToStr to convert the float according the provided locale.

Note that this code doesn't change the locale's decimal separator because we're operating on a copy of the locale information. Therefore we won't trample on any other parts of the program that may need the the correct decimal separator for the locale.

One further tweak suggests itself that takes advantage of the fact that the Format routine can take an optional TFormatSettings parameter:

var
  FS: TFormatSettings;
begin
  GetLocaleFormatSettings(GetThreadLocale, FS);
  FS.DecimalSeparator := '.';
  ExecJS(
    Format('showNum(%.8f, %.8f);', [123.456, -42.56], FS)
  );
end;

And finally, the promised implementation of the ExecJS method. Note that this is a method of the form that contains the browser control, which we assume is named WebBrowser1.

procedure TForm1.ExecJS(const Fn: string);
var
  Doc: IHTMLDocument2;      // current HTML document
  HTMLWindow: IHTMLWindow2; // parent window of current HTML document
begin
  // Get reference to current document
  Doc := WebBrowser1.Document as IHTMLDocument2;
  Assert(Assigned(Doc));
  // Get parent window of current document
  HTMLWindow := Doc.parentWindow;
  Assert(Assigned(HTMLWindow));
  // Run JavaScript
  HTMLWindow.execScript(Fn, 'JavaScript') // execute function
end;

4 comments:

Emyleen said...

Hello, I just read your article on the execution of a JavaScript
in Delphi, it is very interesting!

I wonder if you know a way to recover a Delphi
value returned by a javascript function?

Thank you in advance:)

DelphiDabbler said...

I have an old blog post called Call JavaScript in a TWebBrowser and get a result back that might be what you want. Also read the comments to that post - there's an alternative suggestion there.

Hope that helps.

jowdjbrown said...

So you have to assemble a string containing the function call and its parameters, something like this:psd to drupal

Peter Johnson said...

jowdjbrown: Something wrong with you comment - missing text or broken link!