[PATCH v3 1/7] jscript: Implement Number.prototype.toLocaleString.

Gabriel Ivăncescu gabrielopcode at gmail.com
Tue May 3 10:17:06 CDT 2022


Signed-off-by: Gabriel Ivăncescu <gabrielopcode at gmail.com>
---

VariantChangeTypeEx from VT_R8 to VT_BSTR uses the same method with
_create_locale and _swprintf_l, but with unusable format for this, so I
think it should be fine.

 dlls/jscript/jscript.h     |  1 +
 dlls/jscript/number.c      | 84 +++++++++++++++++++++++++++++++++++++-
 dlls/jscript/tests/api.js  |  7 ++++
 dlls/jscript/tests/run.c   | 37 ++++++++++++++++-
 dlls/mshtml/tests/es5.js   | 50 +++++++++++++++++++++++
 dlls/mshtml/tests/script.c | 36 +++++++++++++++-
 6 files changed, 211 insertions(+), 4 deletions(-)

diff --git a/dlls/jscript/jscript.h b/dlls/jscript/jscript.h
index 7ed4425..90d8142 100644
--- a/dlls/jscript/jscript.h
+++ b/dlls/jscript/jscript.h
@@ -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;
diff --git a/dlls/jscript/number.c b/dlls/jscript/number.c
index be733fb..9470fbf 100644
--- a/dlls/jscript/number.c
+++ b/dlls/jscript/number.c
@@ -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,
diff --git a/dlls/jscript/tests/api.js b/dlls/jscript/tests/api.js
index 1efc023..8653946 100644
--- a/dlls/jscript/tests/api.js
+++ b/dlls/jscript/tests/api.js
@@ -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");
diff --git a/dlls/jscript/tests/run.c b/dlls/jscript/tests/run.c
index e4408be..ebdc8dd 100644
--- a/dlls/jscript/tests/run.c
+++ b/dlls/jscript/tests/run.c
@@ -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();
diff --git a/dlls/mshtml/tests/es5.js b/dlls/mshtml/tests/es5.js
index 4892207..4364054 100644
--- a/dlls/mshtml/tests/es5.js
+++ b/dlls/mshtml/tests/es5.js
@@ -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);
diff --git a/dlls/mshtml/tests/script.c b/dlls/mshtml/tests/script.c
index ceb4fc7..7bf2eb6 100644
--- a/dlls/mshtml/tests/script.c
+++ b/dlls/mshtml/tests/script.c
@@ -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");
-- 
2.34.1




More information about the wine-devel mailing list