jscript: Implement Number.prototype.toLocaleString.

Signed-off-by: Gabriel Ivăncescu <gabrielopcode@gmail.com>
Signed-off-by: Jacek Caban <jacek@codeweavers.com>
Signed-off-by: Alexandre Julliard <julliard@winehq.org>
This commit is contained in:
Gabriel Ivăncescu 2022-05-03 18:17:06 +03:00 committed by Alexandre Julliard
parent 2921714f3c
commit 8aefdf48f9
6 changed files with 211 additions and 4 deletions

View file

@ -449,6 +449,7 @@ HRESULT regexp_string_match(script_ctx_t*,jsdisp_t*,jsstr_t*,jsval_t*) DECLSPEC_
BOOL bool_obj_value(jsdisp_t*) DECLSPEC_HIDDEN;
unsigned array_get_length(jsdisp_t*) DECLSPEC_HIDDEN;
HRESULT localize_number(script_ctx_t*,DOUBLE,BOOL,jsstr_t**) DECLSPEC_HIDDEN;
HRESULT JSGlobal_eval(script_ctx_t*,jsval_t,WORD,unsigned,jsval_t*,jsval_t*) DECLSPEC_HIDDEN;
HRESULT Object_get_proto_(script_ctx_t*,jsval_t,WORD,unsigned,jsval_t*,jsval_t*) DECLSPEC_HIDDEN;

View file

@ -17,6 +17,7 @@
*/
#include <math.h>
#include <locale.h>
#include <assert.h>
#include "jscript.h"
@ -341,11 +342,90 @@ static HRESULT Number_toString(script_ctx_t *ctx, jsval_t vthis, WORD flags, uns
return S_OK;
}
HRESULT localize_number(script_ctx_t *ctx, DOUBLE val, BOOL new_format, jsstr_t **ret)
{
WCHAR buf[316], decimal[8], thousands[8], *numstr;
NUMBERFMTW *format = NULL, format_buf;
LCID lcid = ctx->lcid;
_locale_t locale;
unsigned convlen;
jsstr_t *str;
int len;
/* FIXME: Localize this */
if(!isfinite(val))
return to_string(ctx, jsval_number(val), ret);
/* Native never uses an exponent, even if the number is very large, it will in fact
return all the digits (with thousands separators). jscript.dll uses two digits for
fraction even if they are zero (likely default numDigits) and always returns them,
while mshtml's jscript uses 3 digits and trims trailing zeros (on same locale).
This is even for very small numbers, such as 0.0000999, which will simply be 0. */
if(!(locale = _create_locale(LC_ALL, "C")))
return E_OUTOFMEMORY;
len = _swprintf_l(buf, ARRAY_SIZE(buf), L"%.3f", locale, val);
_free_locale(locale);
if(new_format) {
WCHAR grouping[10];
format = &format_buf;
format->NumDigits = 3;
while(buf[--len] == '0')
format->NumDigits--;
/* same logic as VarFormatNumber */
grouping[2] = '\0';
if(!GetLocaleInfoW(lcid, LOCALE_SGROUPING, grouping, ARRAY_SIZE(grouping)))
format->Grouping = 3;
else
format->Grouping = (grouping[2] == '2' ? 32 : grouping[0] - '0');
if(!GetLocaleInfoW(lcid, LOCALE_ILZERO | LOCALE_RETURN_NUMBER, (WCHAR*)&format->LeadingZero, 2))
format->LeadingZero = 0;
if(!GetLocaleInfoW(lcid, LOCALE_INEGNUMBER | LOCALE_RETURN_NUMBER, (WCHAR*)&format->NegativeOrder, 2))
format->NegativeOrder = 1;
format->lpDecimalSep = decimal;
if(!GetLocaleInfoW(lcid, LOCALE_SDECIMAL, decimal, ARRAY_SIZE(decimal)))
wcscpy(decimal, L".");
format->lpThousandSep = thousands;
if(!GetLocaleInfoW(lcid, LOCALE_STHOUSAND, thousands, ARRAY_SIZE(thousands)))
wcscpy(thousands, L",");
}
if(!(convlen = GetNumberFormatW(lcid, 0, buf, format, NULL, 0)) ||
!(str = jsstr_alloc_buf(convlen - 1, &numstr)))
return E_OUTOFMEMORY;
if(!GetNumberFormatW(lcid, 0, buf, format, numstr, convlen)) {
jsstr_release(str);
return E_OUTOFMEMORY;
}
*ret = str;
return S_OK;
}
static HRESULT Number_toLocaleString(script_ctx_t *ctx, jsval_t vthis, WORD flags, unsigned argc, jsval_t *argv,
jsval_t *r)
{
FIXME("\n");
return E_NOTIMPL;
jsstr_t *str;
HRESULT hres;
DOUBLE val;
TRACE("\n");
hres = numberval_this(vthis, &val);
if(FAILED(hres))
return hres;
if(r) {
hres = localize_number(ctx, val, ctx->version >= SCRIPTLANGUAGEVERSION_ES5, &str);
if(FAILED(hres))
return hres;
*r = jsval_string(str);
}
return S_OK;
}
static HRESULT Number_toFixed(script_ctx_t *ctx, jsval_t vthis, WORD flags, unsigned argc, jsval_t *argv,

View file

@ -1365,6 +1365,11 @@ ok(tmp === "0", "num().toString = " + tmp);
tmp = (new Number(5.5)).toString(2);
ok(tmp === "101.1", "num(5.5).toString(2) = " + tmp);
tmp = (new Number(12)).toLocaleString();
ok(tmp.indexOf(String.fromCharCode(0)) == -1, "invalid null byte");
tmp = Number.prototype.toLocaleString.call(NaN);
ok(tmp.indexOf(String.fromCharCode(0)) == -1, "invalid null byte");
tmp = (new Number(3)).toFixed(3);
ok(tmp === "3.000", "num(3).toFixed(3) = " + tmp);
tmp = (new Number(3)).toFixed();
@ -2594,6 +2599,8 @@ testException(function() {arr.test();}, "E_NO_PROPERTY");
testException(function() {[1,2,3].sort(nullDisp);}, "E_JSCRIPT_EXPECTED");
testException(function() {Number.prototype.toString.call(arr);}, "E_NOT_NUM");
testException(function() {Number.prototype.toFixed.call(arr);}, "E_NOT_NUM");
testException(function() {Number.prototype.toLocaleString.call(arr);}, "E_NOT_NUM");
testException(function() {Number.prototype.toLocaleString.call(null);}, "E_NOT_NUM");
testException(function() {(new Number(3)).toString(1);}, "E_INVALID_CALL_ARG");
testException(function() {(new Number(3)).toFixed(21);}, "E_FRACTION_DIGITS_OUT_OF_RANGE");
testException(function() {(new Number(1)).toPrecision(0);}, "E_PRECISION_OUT_OF_RANGE");

View file

@ -186,6 +186,7 @@ static BOOL strict_dispid_check, testing_expr;
static const char *test_name = "(null)";
static IDispatch *script_disp;
static int invoke_version;
static BOOL use_english;
static IActiveScriptError *script_error;
static IActiveScript *script_engine;
static const CLSID *engine_clsid = &CLSID_JScript;
@ -1834,7 +1835,7 @@ static ULONG WINAPI ActiveScriptSite_Release(IActiveScriptSite *iface)
static HRESULT WINAPI ActiveScriptSite_GetLCID(IActiveScriptSite *iface, LCID *plcid)
{
*plcid = GetUserDefaultLCID();
*plcid = use_english ? MAKELCID(MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), SORT_DEFAULT) : GetUserDefaultLCID();
return S_OK;
}
@ -2996,6 +2997,39 @@ static void test_default_value(void)
close_script(script);
}
static void test_number_localization(void)
{
static struct {
const WCHAR *num;
const WCHAR *expect;
} tests[] = {
{ L"0", L"0.00" },
{ L"+1234.5", L"1,234.50" },
{ L"-1337.7331", L"-1,337.73" },
{ L"-0.0123", L"-0.01" },
{ L"-0.0198", L"-0.02" },
{ L"0.004", L"0.00" },
{ L"65536.5", L"65,536.50" },
{ L"NaN", L"NaN" }
};
static const WCHAR fmt[] = L"Number.prototype.toLocaleString.call(%s)";
WCHAR script_buf[ARRAY_SIZE(fmt) + 32];
HRESULT hres;
unsigned i;
VARIANT v;
use_english = TRUE;
for(i = 0; i < ARRAY_SIZE(tests); i++) {
swprintf(script_buf, ARRAY_SIZE(script_buf), fmt, tests[i].num);
hres = parse_script_expr(script_buf, &v, NULL);
ok(hres == S_OK, "[%u] parse_script_expr failed: %08lx\n", i, hres);
ok(V_VT(&v) == VT_BSTR, "[%u] V_VT(v) = %d\n", i, V_VT(&v));
ok(!lstrcmpW(V_BSTR(&v), tests[i].expect), "[%u] got %s\n", i, wine_dbgstr_w(V_BSTR(&v)));
VariantClear(&v);
}
use_english = FALSE;
}
static void test_script_exprs(void)
{
VARIANT v;
@ -3060,6 +3094,7 @@ static void test_script_exprs(void)
ok(!lstrcmpW(V_BSTR(&v), L"wine"), "V_BSTR(v) = %s\n", wine_dbgstr_w(V_BSTR(&v)));
VariantClear(&v);
test_number_localization();
test_default_value();
test_propputref();
test_retval();

View file

@ -28,6 +28,7 @@ var JS_E_REGEXP_EXPECTED = 0x800a1398;
var JS_E_INVALID_WRITABLE_PROP_DESC = 0x800a13ac;
var JS_E_NONCONFIGURABLE_REDEFINED = 0x800a13d6;
var JS_E_NONWRITABLE_MODIFIED = 0x800a13d7;
var JS_E_WRONG_THIS = 0x800a13fc;
var tests = [];
@ -68,6 +69,55 @@ sync_test("toISOString", function() {
expect_exception(function() { new Date(31494784780800001).toISOString(); });
});
sync_test("Number toLocaleString", function() {
var r = Number.prototype.toLocaleString.length;
ok(r === 0, "length = " + r);
var tests = [
[ 0.0, "0" ],
[ 1234.5, "1,234.5" ],
[ -1337.7331, "-1,337.733" ],
[ -0.0123, "-0.012" ],
[-0.0198, "-0.02" ],
[ 0.004, "0.004" ],
[ 99.004, "99.004" ],
[ 99.0004, "99" ],
[ 65536.5, "65,536.5" ],
[ NaN, "NaN" ]
];
if(external.isEnglish) {
for(var i = 0; i < tests.length; i++) {
r = Number.prototype.toLocaleString.call(tests[i][0]);
ok(r === tests[i][1], "[" + i + "] got " + r);
}
}
try {
Number.prototype.toLocaleString.call("50");
ok(false, "expected exception calling it on string");
}catch(ex) {
var n = ex.number >>> 0;
todo_wine.
ok(n === JS_E_WRONG_THIS, "called on string threw " + n);
}
try {
Number.prototype.toLocaleString.call(undefined);
ok(false, "expected exception calling it on undefined");
}catch(ex) {
var n = ex.number >>> 0;
todo_wine.
ok(n === JS_E_WRONG_THIS, "called on undefined threw " + n);
}
try {
Number.prototype.toLocaleString.call(external.nullDisp);
ok(false, "expected exception calling it on nullDisp");
}catch(ex) {
var n = ex.number >>> 0;
todo_wine.
ok(n === JS_E_WRONG_THIS, "called on nullDisp threw " + n);
}
});
sync_test("indexOf", function() {
function expect(array, args, exr) {
var r = Array.prototype.indexOf.apply(array, args);

View file

@ -153,13 +153,14 @@ DEFINE_EXPECT(GetTypeInfo);
#define DISPID_EXTERNAL_WRITESTREAM 0x300006
#define DISPID_EXTERNAL_GETVT 0x300007
#define DISPID_EXTERNAL_NULL_DISP 0x300008
#define DISPID_EXTERNAL_IS_ENGLISH 0x300009
static const GUID CLSID_TestScript =
{0x178fc163,0xf585,0x4e24,{0x9c,0x13,0x4b,0xb7,0xfa,0xf8,0x07,0x46}};
static const GUID CLSID_TestActiveX =
{0x178fc163,0xf585,0x4e24,{0x9c,0x13,0x4b,0xb7,0xfa,0xf8,0x06,0x46}};
static BOOL is_ie9plus;
static BOOL is_ie9plus, is_english;
static IHTMLDocument2 *notif_doc;
static IOleDocumentView *view;
static IDispatchEx *window_dispex;
@ -599,6 +600,10 @@ static HRESULT WINAPI externalDisp_GetDispID(IDispatchEx *iface, BSTR bstrName,
*pid = DISPID_EXTERNAL_NULL_DISP;
return S_OK;
}
if(!lstrcmpW(bstrName, L"isEnglish")) {
*pid = DISPID_EXTERNAL_IS_ENGLISH;
return S_OK;
}
ok(0, "unexpected name %s\n", wine_dbgstr_w(bstrName));
return DISP_E_UNKNOWNNAME;
@ -784,6 +789,21 @@ static HRESULT WINAPI externalDisp_InvokeEx(IDispatchEx *iface, DISPID id, LCID
V_DISPATCH(pvarRes) = NULL;
return S_OK;
case DISPID_EXTERNAL_IS_ENGLISH:
ok(wFlags == INVOKE_PROPERTYGET, "wFlags = %x\n", wFlags);
ok(pdp != NULL, "pdp == NULL\n");
ok(!pdp->rgvarg, "rgvarg != NULL\n");
ok(!pdp->rgdispidNamedArgs, "rgdispidNamedArgs != NULL\n");
ok(!pdp->cArgs, "cArgs = %d\n", pdp->cArgs);
ok(!pdp->cNamedArgs, "cNamedArgs = %d\n", pdp->cNamedArgs);
ok(pvarRes != NULL, "pvarRes == NULL\n");
ok(V_VT(pvarRes) == VT_EMPTY, "V_VT(pvarRes) = %d\n", V_VT(pvarRes));
ok(pei != NULL, "pei == NULL\n");
V_VT(pvarRes) = VT_BOOL;
V_BOOL(pvarRes) = is_english ? VARIANT_TRUE : VARIANT_FALSE;
return S_OK;
default:
ok(0, "unexpected call\n");
return E_NOTIMPL;
@ -3743,6 +3763,19 @@ static HWND create_container_window(void)
300, 300, NULL, NULL, NULL, NULL);
}
static void detect_locale(void)
{
HMODULE kernel32 = GetModuleHandleA("kernel32.dll");
LANGID (WINAPI *pGetThreadUILanguage)(void) = (void*)GetProcAddress(kernel32, "GetThreadUILanguage");
is_english = ((!pGetThreadUILanguage || PRIMARYLANGID(pGetThreadUILanguage()) == LANG_ENGLISH) &&
PRIMARYLANGID(GetUserDefaultUILanguage()) == LANG_ENGLISH &&
PRIMARYLANGID(GetUserDefaultLangID()) == LANG_ENGLISH);
if(!is_english)
skip("Skipping some tests in non-English locale\n");
}
static BOOL check_ie(void)
{
IHTMLDocument2 *doc;
@ -3779,6 +3812,7 @@ START_TEST(script)
CoInitialize(NULL);
container_hwnd = create_container_window();
detect_locale();
if(argc > 2) {
init_protocol_handler();
run_script_as_http_with_mode(argv[2], NULL, "11");