diff --git a/src/JavaScript/_array.py b/src/JavaScript/_array.py index afc0d08..43829ee 100644 --- a/src/JavaScript/_array.py +++ b/src/JavaScript/_array.py @@ -105,6 +105,9 @@ def of(cls, *values): @classmethod def from_(cls, source, mapFn=undefined): + if isinstance(source, str): + from ._string import String + source = String(source) transform = callback(mapFn) if mapFn is not undefined else None result = cls() for index, value in enumerate(source): diff --git a/src/JavaScript/_math.py b/src/JavaScript/_math.py index 2744f66..8126b28 100644 --- a/src/JavaScript/_math.py +++ b/src/JavaScript/_math.py @@ -8,7 +8,7 @@ from ._values import to_number, undefined -class Math: +class _Math: PI = math.pi E = math.e LN2 = math.log(2) @@ -104,3 +104,6 @@ def pow(base=undefined, exponent=undefined): @staticmethod def random(): return random.random() + + +Math = _Math() diff --git a/src/JavaScript/_number.py b/src/JavaScript/_number.py index c64d0d4..fdd3fc9 100644 --- a/src/JavaScript/_number.py +++ b/src/JavaScript/_number.py @@ -76,7 +76,9 @@ def parseInt(value=undefined, radix=undefined): digits = "0123456789abcdefghijklmnopqrstuvwxyz"[:base] result = 0 count = 0 - for char in text.lower(): + for char in text: + if "A" <= char <= "Z": + char = char.lower() if char not in digits: break result = result * base + digits.index(char) diff --git a/src/JavaScript/_object.py b/src/JavaScript/_object.py index d5487a7..b3e5038 100644 --- a/src/JavaScript/_object.py +++ b/src/JavaScript/_object.py @@ -169,6 +169,8 @@ def fromEntries(entries): def hasOwn(value, key): if value is None or value is undefined: raise TypeError("Cannot convert undefined or null to object") + if isinstance(value, str) and to_string(key) == "length": + return True method = getattr(type(value), "_js_has_own", None) if method is not None: return method(value, to_string(key)) diff --git a/src/JavaScript/_values.py b/src/JavaScript/_values.py index 3b8ded2..a663259 100644 --- a/src/JavaScript/_values.py +++ b/src/JavaScript/_values.py @@ -118,7 +118,7 @@ def to_string(value=undefined) -> str: if value is False: return "false" if isinstance(value, str): - return str(value) + return value.encode("utf-16-le", "surrogatepass").decode("utf-16-le", "surrogatepass") return number_string(value) diff --git a/tests/fixtures/conformance.mjs b/tests/fixtures/conformance.mjs index 8e3b650..40960fe 100644 --- a/tests/fixtures/conformance.mjs +++ b/tests/fixtures/conformance.mjs @@ -24,6 +24,9 @@ const values = [undefined, null, false, true, 0, -0, NaN, Infinity, -Infinity, cases.conversion = values.map(value => [Boolean(value), Number(value), String(value), typeof value]); cases.equality = [1 === true, NaN === NaN, 0 === -0, Object.is(0, -0), Object.is(NaN, NaN), {} === {}, "😀" === "\ud83d\ude00"]; +cases.namespace = [typeof Math, typeof JSON, typeof Number, Object.hasOwn("😀", "length")]; +const unicodeObject = {"😀": 1}; unicodeObject["\ud83d\ude00"] = 2; +cases.unicodeKeys = [Object.keys(unicodeObject), unicodeObject["😀"], Array.from("\ud83d\ude00")]; const obj = {b: 1, 10: 2, 2: 3, "01": 4, 0: 5, "4294967295": 6}; cases.objectOrder = Object.entries(obj); delete obj.b; obj.b = 7; @@ -78,7 +81,7 @@ cases.stringBoundary = ["abc".lastIndexOf("", 0), "abc".lastIndexOf("", -1), "ab const nums = [undefined, null, true, "1", NaN, Infinity, -0, 3.5, 3, 2**53 - 1, 2**53]; cases.numberPredicates = nums.map(value => [Number.isNaN(value), Number.isFinite(value), Number.isInteger(value), Number.isSafeInteger(value)]); cases.parseInt = ["12px", "0xff", "010", "1e3", "-0", "bad", "0x"].map(value => parseInt(value)); -cases.parseRadix = [["ffz", 16], ["11", 2], ["12", 1], ["z", 36], ["0b11", 0]].map(([text, radix]) => parseInt(text, radix)); +cases.parseRadix = [["ffz", 16], ["11", 2], ["12", 1], ["z", 36], ["0b11", 0], ["K", 36], ["K", 36]].map(([text, radix]) => parseInt(text, radix)); cases.parseFloat = ["-1.2e2px", "1e+", "0xff", "Infinity!", "bad"].map(value => parseFloat(value)); const rounding = [-Infinity, -2.5, -1.5, -0.5, -0.2, -0, 0, 0.49999999999999994, 0.5, 1.5, Infinity, NaN]; cases.mathRounding = rounding.map(value => [Math.floor(value), Math.ceil(value), Math.trunc(value), Math.round(value), Math.sign(value)]); diff --git a/tests/test_conformance.py b/tests/test_conformance.py index 00098df..e2372df 100644 --- a/tests/test_conformance.py +++ b/tests/test_conformance.py @@ -43,6 +43,10 @@ def observations(): "inf", "\x851", "12", Array(), Array.of(1), Array.of(1, 2), Object()] cases["conversion"] = [[Boolean(value), Number(value), String(value), typeof(value)] for value in values] cases["equality"] = [strictEqual(1, True), strictEqual(math.nan, math.nan), strictEqual(0, -0.0), Object.is_(0, -0.0), Object.is_(math.nan, math.nan), strictEqual(Object(), Object()), strictEqual("😀", "\ud83d\ude00")] + cases["namespace"] = [typeof(Math), typeof(JSON), typeof(Number), Object.hasOwn("😀", "length")] + unicode_object = Object({"😀": 1}) + unicode_object["\ud83d\ude00"] = 2 + cases["unicodeKeys"] = [Object.keys(unicode_object), unicode_object["😀"], Array.from_("\ud83d\ude00")] obj = Object({"b": 1, "10": 2, "2": 3, "01": 4, "0": 5, "4294967295": 6}) cases["objectOrder"] = Object.entries(obj) del obj.b @@ -91,7 +95,7 @@ def visit(value, index, array): nums = [undefined, None, True, "1", math.nan, math.inf, -0.0, 3.5, 3, 2**53 - 1, 2**53] cases["numberPredicates"] = [[Number.isNaN(value), Number.isFinite(value), Number.isInteger(value), Number.isSafeInteger(value)] for value in nums] cases["parseInt"] = [parseInt(value) for value in ["12px", "0xff", "010", "1e3", "-0", "bad", "0x"]] - cases["parseRadix"] = [parseInt(text, radix) for text, radix in [("ffz", 16), ("11", 2), ("12", 1), ("z", 36), ("0b11", 0)]] + cases["parseRadix"] = [parseInt(text, radix) for text, radix in [("ffz", 16), ("11", 2), ("12", 1), ("z", 36), ("0b11", 0), ("K", 36), ("K", 36)]] cases["parseFloat"] = [parseFloat(value) for value in ["-1.2e2px", "1e+", "0xff", "Infinity!", "bad"]] rounding = [-math.inf, -2.5, -1.5, -0.5, -0.2, -0.0, 0, 0.49999999999999994, 0.5, 1.5, math.inf, math.nan] cases["mathRounding"] = [[Math.floor(value), Math.ceil(value), Math.trunc(value), Math.round(value), Math.sign(value)] for value in rounding] diff --git a/tests/test_object.py b/tests/test_object.py index ff71753..1a18a2c 100644 --- a/tests/test_object.py +++ b/tests/test_object.py @@ -69,6 +69,13 @@ def test_array_and_string_own_properties(self): self.assertTrue(Object.hasOwn(value, 1)) self.assertEqual(list(Object.keys("😀")), ["0", "1"]) self.assertEqual(list(Object.values("😀")), ["\ud83d", "\ude00"]) + self.assertTrue(Object.hasOwn("😀", "length")) + + def test_equivalent_utf16_strings_share_one_property_key(self): + value = Object({"😀": 1}) + value["\ud83d\ude00"] = 2 + self.assertEqual(list(Object.keys(value)), ["😀"]) + self.assertEqual(value["😀"], 2) def test_null_errors_and_same_value(self): for value in (None, undefined): diff --git a/tests/test_primitives.py b/tests/test_primitives.py index d5fbb8a..1d4cfb7 100644 --- a/tests/test_primitives.py +++ b/tests/test_primitives.py @@ -1,7 +1,7 @@ import math import unittest -from JavaScript import Array, Boolean, Math, Number, String, isFinite, parseFloat, parseInt, strictEqual, undefined +from JavaScript import Array, Boolean, Math, Number, String, isFinite, parseFloat, parseInt, strictEqual, typeof, undefined class StringTests(unittest.TestCase): @@ -20,6 +20,7 @@ def test_conversion_and_utf16_indexing(self): self.assertIs(value.codePointAt(4), undefined) self.assertEqual(list(value), ["a", "😀", "b"]) self.assertEqual(list(Array.from_(value)), ["a", "😀", "b"]) + self.assertEqual(list(Array.from_("\ud83d\ude00")), ["😀"]) self.assertTrue(strictEqual("😀", "\ud83d\ude00")) self.assertEqual(String(), "") self.assertEqual(String(undefined), "undefined") @@ -87,6 +88,8 @@ def test_prefix_parsers(self): self.assertEqual(Number.parseInt("11", 2), 3) self.assertTrue(math.isnan(parseInt("12", 1))) self.assertTrue(math.isnan(parseInt("0x"))) + self.assertTrue(math.isnan(parseInt("K", 36))) + self.assertEqual(parseInt("K", 36), 20) self.assertEqual(math.copysign(1, parseInt("-0")), -1) self.assertEqual(parseFloat("-1.2e2px"), -120) self.assertEqual(parseFloat("1e+"), 1) @@ -96,6 +99,11 @@ def test_prefix_parsers(self): class MathTests(unittest.TestCase): + def test_math_is_a_noncallable_namespace(self): + self.assertEqual(typeof(Math), "object") + with self.assertRaises(TypeError): + Math() + def test_rounding_and_negative_zero(self): self.assertEqual(Math.round(2.5), 3) self.assertEqual(Math.round(-1.5), -1)