Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ Auto-generated by `script/capability-matrix.php`. Do not edit by hand.
| `sha1` | yes | yes | yes | standard | JIT PHPT; AOT PHPT |
| `shell_exec` | yes | yes | yes | standard | |
| `shuffle` | yes | yes | yes | standard | JIT PHPT; AOT PHPT |
| `similar_text` | yes | yes | yes | standard | JIT PHPT; AOT PHPT |
| `sin` | yes | yes | yes | standard | |
| `sizeof` | yes | yes | yes | standard | JIT PHPT; AOT PHPT |
| `sleep` | yes | yes | yes | standard | JIT PHPT |
Expand Down
1 change: 1 addition & 0 deletions docs/stdlib-jit-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ _None — all JIT `call()` builtins are lowered or deferred._
- `krsort` — `ext/standard/krsort_.php`
- `lcfirst` — `ext/standard/lcfirst.php`
- `levenshtein` — `ext/standard/levenshtein.php`
- `similar_text` — `ext/standard/similar_text.php`
- `soundex` — `ext/standard/soundex.php`
- `log` — `ext/standard/log.php`
- `lstat` — `ext/standard/lstat_.php`
Expand Down
1 change: 1 addition & 0 deletions ext/standard/Module.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public function getFunctions(): array
new chr(),
new strcmp(),
new levenshtein(),
new similar_text(),
new soundex(),
new metaphone(),
new strnatcmp(),
Expand Down
89 changes: 89 additions & 0 deletions ext/standard/VmString.php
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,95 @@ public static function levenshtein(
return $prev[$len2];
}

/**
* similar_text() — PHP-compatible Oliver algorithm (issue #2445).
*/
public static function similar_text(string $string1, string $string2, ?float &$percent = null): int
{
$len1 = self::byteLength($string1);
$len2 = self::byteLength($string2);
if ($len1 > 255 || $len2 > 255) {
throw new \ValueError(
'similar_text(): Argument #1 ($string1) or #2 ($string2) must be less than 256 characters'
);
}
if (0 === $len1 && 0 === $len2) {
if (null !== $percent) {
$percent = 0.0;
}

return 0;
}
$sim = self::similarChar($string1, $len1, $string2, $len2);
if (null !== $percent) {
$percent = $sim * 200.0 / ($len1 + $len2);
}

return $sim;
}

private static function similarStr(
string $txt1,
int $len1,
string $txt2,
int $len2,
int &$pos1,
int &$pos2,
int &$max,
int &$count
): void {
$max = 0;
$count = 0;
for ($p = 0; $p < $len1; ++$p) {
for ($q = 0; $q < $len2; ++$q) {
$l = 0;
while (
$p + $l < $len1
&& $q + $l < $len2
&& $txt1[$p + $l] === $txt2[$q + $l]
) {
++$l;
}
if ($l > $max) {
$max = $l;
++$count;
$pos1 = $p;
$pos2 = $q;
}
}
}
}

private static function similarChar(string $txt1, int $len1, string $txt2, int $len2): int
{
$pos1 = 0;
$pos2 = 0;
$max = 0;
$count = 0;
self::similarStr($txt1, $len1, $txt2, $len2, $pos1, $pos2, $max, $count);
$sum = $max;
if ($sum > 0) {
if ($pos1 > 0 && $pos2 > 0 && $count > 1) {
$sum += self::similarChar(
substr($txt1, 0, $pos1),
$pos1,
substr($txt2, 0, $pos2),
$pos2
);
}
if ($pos1 + $max < $len1 && $pos2 + $max < $len2) {
$sum += self::similarChar(
substr($txt1, $pos1 + $max),
$len1 - $pos1 - $max,
substr($txt2, $pos2 + $max),
$len2 - $pos2 - $max
);
}
}

return $sum;
}

/**
* metaphone() — PHP-compatible Metaphone on ASCII letters (issue #2423).
*/
Expand Down
70 changes: 70 additions & 0 deletions ext/standard/similar_text.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\ext\standard;

use PHPCompiler\Frame;
use PHPCompiler\Func\Internal;
use PHPCompiler\JIT\Builtin\StringSimilarText;
use PHPCompiler\JIT\Context;
use PHPCompiler\JIT\Variable as JITVariable;
use PHPCompiler\VM\Variable;
use PHPLLVM\Value;

/**
* similar_text() — similarity between two strings (subset of PHP; issue #2445).
*/
final class similar_text extends Internal
{
public function __construct()
{
parent::__construct('similar_text');
}

public function execute(Frame $frame): void
{
$argc = \count($frame->calledArgs);
if ($argc < 2 || $argc > 3) {
throw new \LogicException('similar_text() accepts two or three arguments in this compiler build');
}
$a = $frame->calledArgs[0]->resolveIndirect();
$b = $frame->calledArgs[1]->resolveIndirect();
if (Variable::TYPE_STRING !== $a->type || Variable::TYPE_STRING !== $b->type) {
throw new \LogicException('similar_text() requires two strings in this compiler build');
}
$s1 = $a->toString();
$s2 = $b->toString();
if (3 === $argc) {
$percent = 0.0;
$sim = VmString::similar_text($s1, $s2, $percent);
$frame->calledArgs[2]->resolveIndirect()->float($percent);
} else {
$sim = VmString::similar_text($s1, $s2);
}
if (null === $frame->returnVar) {
return;
}
$frame->returnVar->int($sim);
}

public Context $context;

public function call(Context $context, JITVariable ...$args): Value
{
$this->context = $context;
if (\count($args) !== 2) {
throw new \LogicException(
'similar_text() JIT/AOT only supports two arguments in this compiler build; use bin/vm.php for &$percent'
);
}
StringSimilarText::ensureLinked($context);
$p0 = $this->stringDataPtr($context, $this->jitString($context, $args[0], 'similar_text() argument #1'));
$p1 = $this->stringDataPtr($context, $this->jitString($context, $args[1], 'similar_text() argument #2'));
$fn = $context->lookupFunction('phpc_similar_text');
$i64 = $context->getTypeFromString('int64');
$raw = $context->builder->call($fn, $p0, $p1);

return $context->builder->sExt($raw, $i64);
}
}
1 change: 1 addition & 0 deletions lib/AOT/Linker.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ final class Linker
__DIR__.'/runtime/phpc_strnatcasecmp.c',
__DIR__.'/runtime/phpc_substr_compare.c',
__DIR__.'/runtime/phpc_levenshtein.c',
__DIR__.'/runtime/phpc_similar_text.c',
__DIR__.'/runtime/phpc_soundex.c',
__DIR__.'/runtime/phpc_metaphone.c',
__DIR__.'/runtime/phpc_str_getcsv.c',
Expand Down
1 change: 1 addition & 0 deletions lib/AOT/runtime/builtin_function_names.inc
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ static const char *phpc_builtin_functions[] = {
"setrawcookie",
"sha1",
"shell_exec",
"similar_text",
"shuffle",
"sin",
"sizeof",
Expand Down
94 changes: 94 additions & 0 deletions lib/AOT/runtime/phpc_similar_text.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* similar_text() runtime for VM/JIT/AOT (issue #2445).
* PHP-compatible Oliver algorithm; byte-oriented; max 255 bytes per string.
*/

#include <stddef.h>
#include <stdint.h>
#include <string.h>

#define PHPC_SIM_MAX_LEN 255

static void phpc_similar_str(
const char *txt1,
size_t len1,
const char *txt2,
size_t len2,
size_t *pos1,
size_t *pos2,
size_t *max,
size_t *count
)
{
const char *p;
const char *q;
const char *end1 = txt1 + len1;
const char *end2 = txt2 + len2;
size_t l;

*max = 0;
*count = 0;
for (p = txt1; p < end1; p++) {
for (q = txt2; q < end2; q++) {
for (l = 0; (p + l < end1) && (q + l < end2) && (p[l] == q[l]); l++) {
}
if (l > *max) {
*max = l;
*count += 1;
*pos1 = (size_t) (p - txt1);
*pos2 = (size_t) (q - txt2);
}
}
}
}

static size_t phpc_similar_char(const char *txt1, size_t len1, const char *txt2, size_t len2)
{
size_t sum;
size_t pos1 = 0;
size_t pos2 = 0;
size_t max = 0;
size_t count = 0;

phpc_similar_str(txt1, len1, txt2, len2, &pos1, &pos2, &max, &count);
sum = max;
if (sum) {
if (pos1 && pos2 && count > 1) {
sum += phpc_similar_char(txt1, pos1, txt2, pos2);
}
if ((pos1 + max < len1) && (pos2 + max < len2)) {
sum += phpc_similar_char(
txt1 + pos1 + max,
len1 - pos1 - max,
txt2 + pos2 + max,
len2 - pos2 - max
);
}
}

return sum;
}

int phpc_similar_text(const char *s1, const char *s2)
{
size_t len1;
size_t len2;

if (NULL == s1) {
s1 = "";
}
if (NULL == s2) {
s2 = "";
}

len1 = strlen(s1);
len2 = strlen(s2);
if (len1 > PHPC_SIM_MAX_LEN || len2 > PHPC_SIM_MAX_LEN) {
return -1;
}
if (0 == len1 && 0 == len2) {
return 0;
}

return (int) phpc_similar_char(s1, len1, s2, len2);
}
1 change: 1 addition & 0 deletions lib/BuiltinParamNames.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ final class BuiltinParamNames
private const MAP = [
'strlen' => ['string'],
'parse_str' => ['string', 'array'],
'similar_text' => ['string1', 'string2', 'percent'],
];

/**
Expand Down
Loading