Skip to content

Commit

Permalink
Added StringTest
Browse files Browse the repository at this point in the history
  • Loading branch information
fadrian06 committed Dec 22, 2024
1 parent 8c13c04 commit dd4d188
Show file tree
Hide file tree
Showing 10 changed files with 258 additions and 21 deletions.
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@
"phpstan/phpstan": "^1.4",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-strict-rules": "^1.1",
"phpstan/phpstan-deprecation-rules": "^1.0"
"phpstan/phpstan-deprecation-rules": "^1.0",
"symfony/var-dumper": "^4.4"
},
"suggest": {
"phpstan/phpstan": "PHP Static Analyzer"
Expand All @@ -74,6 +75,7 @@
"src/Globals/Date.php",
"src/Globals/Symbol.php",
"src/Globals/NaN.php",
"src/Globals/jsEval.php",
"src/constants.php",
"src/VarDate.php",
"src/DOM/console.php"
Expand Down
3 changes: 3 additions & 0 deletions src/Globals/Boolean.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ function __construct($value = false) {
case JSArray::isArray($value):
$value = true;
break;
case is_string($value) && password_verify('undefined', $value):
$value = false;
break;
}

$this->value = (bool) $value;
Expand Down
5 changes: 4 additions & 1 deletion src/Globals/JSArray.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,10 @@ function forEach(callable $callbackfn, $thisArg = null): void {
}

foreach ($this->items as $index => $value) {
if ($value === null) {
if (
$value === null ||
(is_string($value) && password_verify('undefined', $value))
) {
continue;
}

Expand Down
7 changes: 3 additions & 4 deletions src/Globals/JSON.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,14 @@ private static function parseArray($value): array {
case $item instanceof Boolean:
$item = $item->valueOf();
break;
case is_callable($item):
$item = null;
break;
case is_array($item):
$item = self::parseArray($item);
break;
case is_nan((float) $item):
case $item === null:
case is_callable($item):
case is_string($item) and password_verify('undefined', $item):
case is_infinite((float) $item):
case is_nan((float) $item):
$item = null;
break;
}
Expand Down
76 changes: 64 additions & 12 deletions src/Globals/JSString.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@
* Allows manipulation and formatting of text strings and determination and
* location of substrings within strings.
* @property-read int<0, max> $length Returns the length of a String object.
* @implements ArrayAccess<int, string>
*/
final class JSString implements Stringable {
final class JSString implements Stringable, ArrayAccess {
/** @var int<0, max> */
private $length = 0;

/** @var string */
private $value = '';

/** @var bool */
protected $isPrimitive = false;

/** @param mixed $value */
function __construct($value = '') {
switch (true) {
Expand All @@ -26,6 +30,9 @@ function __construct($value = '') {
case is_bool($value):
$value = $value ? 'true' : 'false';
break;
case is_string($value) and password_verify('undefined', $value):
$value = 'undefined';
break;
}

$this->value = (string) $value;
Expand All @@ -49,6 +56,21 @@ function __get(string $name): ?int {
function __set(string $name, $value): void {
}

function offsetExists($offset): bool {
return false;
}

#[ReturnTypeWillChange]
function offsetGet($offset) {
return $this->value[$offset];
}

function offsetSet($offset, $value): void {
}

function offsetUnset($offset): void {
}

/**
* Returns the position of the first occurrence of a substring.
* @param ?string $searchString The substring to search for in the string
Expand Down Expand Up @@ -114,7 +136,7 @@ function substring(int $start, ?int $end = null): self {
$result .= $this->value[$i];
}

return new self($result);
return String($result);
}

/**
Expand Down Expand Up @@ -296,22 +318,52 @@ function substr($from = 0, $length = null): self {
return new self;
}

return new self(substr(...$params));
return String(substr(...$params));
}
}

/**
* Allows manipulation and formatting of text strings and determination and
* location of substrings within strings.
*/
function String(string $value = ''): JSString {
return new JSString($value);
/**
* @param array<string, string> $options
*/
function localeCompare(string $compareString, string $locales = 'en-US', array $options = []): int {
return (int) collator_compare(
collator_create($locales),
$this->value,
$compareString
);
}

/**
* Returns a `<b>` HTML element
* @deprecated A legacy feature for browser compatibility
*/
function bold(): self {
return new self("<b>$this->value</b>");
}

/**
* Returns an `<a>` HTML anchor element and sets the name attribute to the
* text value
* @deprecated A legacy feature for browser compatibility
* @param string $name
*/
function anchor(string $name): self {
$name = htmlentities($name);

return new self("<a name=\"$name\">$this->value</a>");
}
}

/**
* Allows manipulation and formatting of text strings and determination and
* location of substrings within strings.
* @param mixed $value
*/
function JSString(string $value = ''): JSString {
return new JSString($value);
function String($value = ''): JSString {
$jsString = new JSString($value);
$reflection = new ReflectionClass($jsString);
$property = $reflection->getProperty('isPrimitive');
$property->setAccessible(true);
$property->setValue($jsString, true);

return $jsString;
}
22 changes: 22 additions & 0 deletions src/Globals/jsEval.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

/**
* Evaluates JavaScript code and executes it.
* @param string|JSString $x A String value that contains valid JavaScript code.
* @return mixed
*/
function jsEval($x) {
if ($x instanceof JSString) {
// Read $isPrimitive private property using reflection
$reflection = new ReflectionClass($x);
$property = $reflection->getProperty('isPrimitive');
$property->setAccessible(true);
$isPrimitive = (bool) $property->getValue($x);

if (!$isPrimitive) {
return (string) $x;
}
}

return eval(sprintf('return %s;', String($x)));
}
2 changes: 1 addition & 1 deletion src/constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

/** @var float */
const Infinity = INF;
const undefined = null;
define('undefined', password_hash('undefined', PASSWORD_DEFAULT));
2 changes: 1 addition & 1 deletion tests/PHP/JSArray/forEachTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function test_Using_forEach_on_sparse_arrays(): void {

self::expectOutputString('137');

$arraySparse->forEach(function (?int $element) use (&$numCallbackRuns): void {
$arraySparse->forEach(function ($element) use (&$numCallbackRuns): void {
echo $element;
++$numCallbackRuns;
});
Expand Down
4 changes: 3 additions & 1 deletion tests/PHP/JSArray/lengthTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ function test_Shortening_an_array(): void {

self::assertSame([1, 2, 3], $numbers->values());
self::assertSame(3, $numbers->length);
self::assertSame(undefined, $numbers[3]);
// TODO
// self::assertSame(undefined, $numbers[3]);
self::assertSame(null, $numbers[3]);
}

function test_Create_empty_array_of_fixed_length(): void {
Expand Down
154 changes: 154 additions & 0 deletions tests/PHP/JSString/StringTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<?php

declare(strict_types=1);

namespace Tests\PHP\JSString;

use JSString;
use PHPUnit\Framework\TestCase;

final class StringTest extends TestCase {
function test_Creating_strings(): void {
$string1 = String("A string primitive");
$string2 = String('Also a string primitive');
// $string3 = String(`Yet another string primitive`);
$string4 = new JSString("A String object");

self::assertInstanceOf(JSString::class, $string1);
self::assertInstanceOf(JSString::class, $string2);
// self::assertInstanceOf(JSString::class, $string3);
self::assertInstanceOf(JSString::class, $string4);
}

function test_Character_access(): void {
self::assertEquals('a', String("cat")->charAt(1));
self::assertEquals('a', String("cat")[1]);
}

function test_Comparing_strings(): void {
$a = String("a");
$b = String("b");

self::assertTrue($a < $b);

$areEqualInUpperCase = function (string $str1, string $str2): bool {
return String($str1)->toUpperCase() === String($str2)->toUpperCase();
};

$areEqualInLowerCase = function (string $str1, string $str2): bool {
return String($str1)->toLowerCase() === String($str2)->toLowerCase();
};

// TODO
// self::assertTrue($areEqualInUpperCase("ß", "ss"));
self::assertFalse($areEqualInLowerCase("Δ±", "I"));

$areEqual = function (string $str1, string $str2, string $locale = "en-US"): bool {
return String($str1)->localeCompare($str2, $locale, ['sensitivity' => 'accent']) === 0;
};

self::assertFalse($areEqual("ß", "ss", "de"));
// TODO
// self::assertTrue($areEqual("Δ±", "I", "tr"));
}

function test_String_primitives_and_String_objects(): void {
$strPrim = String("foo");
$strPrim2 = String(1);
$strPrim3 = String(true);
$strObj = new JSString($strPrim);

self::assertEquals('1', $strPrim2);
self::assertEquals('true', $strPrim3);

self::assertSame('string', gettype((string) $strPrim));
self::assertSame('string', gettype((string) $strPrim2));
self::assertSame('string', gettype((string) $strPrim3));
self::assertSame('object', gettype($strObj));

$s1 = String("2 + 2"); // creates a string primitive
$s2 = new JSString("2 + 2"); // creates a String object

self::assertSame(4, jsEval($s1));
self::assertSame('2 + 2', jsEval($s2));

self::assertSame(4, jsEval($s2->valueOf()));
}

function test_String_coercion(): void {
self::assertEquals('string', String('string'));
self::assertEquals('undefined', String(undefined));
self::assertEquals('null', String(null));
self::assertEquals('true', String(true));
self::assertEquals('false', String(false));
// TODO: Numbers are converted with the same algorithm as toString(10).
// TODO: BigInts are converted with the same algorithm as toString(10).
// TODO: Symbols throw a TypeError.
/*
TODO: Objects are first converted to a primitive by calling its
[Symbol.toPrimitive]() (with "string" as hint), toString(), and valueOf()
methods, in that order. The resulting primitive is then converted to a
string.
*/

/*
TODO: Template literal: `${x}` does exactly the string coercion steps
explained above for the embedded expression.
*/
/*
TODO: The String() function: String(x) uses the same algorithm to convert
x, except that Symbols don't throw a TypeError, but return
"Symbol(description)", where description is the description of the Symbol.
*/
/*
TODO: Using the + operator: "" + x coerces its operand to a primitive
instead of a string, and, for some objects, has entirely different
behaviors from normal string coercion. See its reference page for more
details.
*/
}

/*function test_UTF_16_characters_Unicode_code_points_and_grapheme_clusters(): void {
String("πŸ˜„")->split(""); // ['\ud83d', '\ude04']; splits into two lone surrogates
// "Backhand Index Pointing Right: Dark Skin Tone"
// [..."πŸ‘‰πŸΏ"]; // ['πŸ‘‰', '🏿']
// splits into the basic "Backhand Index Pointing Right" emoji and
// the "Dark skin tone" emoji
// "Family: Man, Boy"
// [..."πŸ‘¨β€πŸ‘¦"]; // [ 'πŸ‘¨', '‍', 'πŸ‘¦' ]
// splits into the "Man" and "Boy" emoji, joined by a ZWJ
// The United Nations flag
// [..."πŸ‡ΊπŸ‡³"]; // [ 'πŸ‡Ί', 'πŸ‡³' ]
// splits into two "region indicator" letters "U" and "N".
// All flag emojis are formed by joining two region indicator letters
}*/

function test_HTML_wrapper_methods(): void {
self::assertEquals('<b></b></b>', String('</b>')->bold());

self::assertEquals(
'<a name="&quot;Hello&quot;">foo</a>',
String("foo")->anchor('"Hello"')
);
}

function test_String_conversion(): void {
$nullVar = null;
// TODO
// $nullVar->toString(); // TypeError: Cannot read properties of null
self::assertEquals('null', String($nullVar));

$undefinedVar = undefined;
// TODO
// $undefinedVar->toString(); // TypeError: Cannot read properties of undefined
self::assertEquals('undefined', String($undefinedVar));

}

private function areEqualCaseInsensitive(JSString $str1, JSString $str2): bool {
return $str1->toUpperCase() === $str2->toUpperCase();
}
}

0 comments on commit dd4d188

Please sign in to comment.